From 674e978c9aa93ea005c9467e991d07cef1bfa49f Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 2 Apr 2025 11:43:42 -0600 Subject: [PATCH 001/116] Add an alternative xarray approach to computing convservation time series This alternative approach makes sure the constents are unique (and sorted) in the xtime variable. To reduce redundancy between the new approach and the old ncrcat approach (retained for comparison and in case the xarray approach is not performant), a new helper function for determining if we want to append to the existing datasets added. --- mpas_analysis/ocean/conservation.py | 151 +++++++++++++++++++++------- 1 file changed, 114 insertions(+), 37 deletions(-) diff --git a/mpas_analysis/ocean/conservation.py b/mpas_analysis/ocean/conservation.py index 74fa3ad8d..6ae6f0f28 100644 --- a/mpas_analysis/ocean/conservation.py +++ b/mpas_analysis/ocean/conservation.py @@ -252,7 +252,7 @@ def run_task(self): for plot_type in self.plotTypes: for varname in self.variableList[plot_type]: all_plots_variable_list.append(varname) - self._compute_time_series_with_ncrcat(all_plots_variable_list) + self._compute_time_series_with_xarray(all_plots_variable_list) for plot_type in self.plotTypes: self._make_plot(plot_type) @@ -604,28 +604,27 @@ def _get_variable(self, ds, varname, mks=False): return variable - def _compute_time_series_with_ncrcat(self, variable_list): - + def _check_output_safe_to_append(self, variable_list): """ - Uses ncrcat to extact time series from conservationCheckOutput files + Check if the output file is safe to append to by verifying the presence + of necessary variables and determining if new input files are needed. - Raises - ------ - OSError - If ``ncrcat`` is not in the system path. + Parameters + ---------- + variable_list : list of str + List of variables to include in the time series. + + Returns + ------- + append : bool + True if the output file can be safely appended to, False otherwise. + inputFiles : list of str + Updated list of input files to process. """ - - if shutil.which('ncrcat') is None: - raise OSError('ncrcat not found. Make sure the latest nco ' - 'package is installed: \n' - 'conda install nco\n' - 'Note: this presumes use of the conda-forge ' - 'channel.') - - inputFiles = self.inputFiles append = False + inputFiles = self.inputFiles + if os.path.exists(self.outputFile): - # make sure all the necessary variables are also present with xr.open_dataset(self.outputFile) as ds: if ds.sizes['Time'] == 0: updateSubset = False @@ -637,11 +636,7 @@ def _compute_time_series_with_ncrcat(self, variable_list): break if updateSubset: - # add only input files with times that aren't already in - # the output file - append = True - fileNames = sorted(self.inputFiles) inYears, inMonths = get_files_year_month( fileNames, self.historyStreams, @@ -652,33 +647,51 @@ def _compute_time_series_with_ncrcat(self, variable_list): totalMonths = 12 * inYears + inMonths dates = decode_strings(ds.xtime) - lastDate = dates[-1] - lastYear = int(lastDate[0:4]) lastMonth = int(lastDate[5:7]) lastTotalMonths = 12 * lastYear + lastMonth - inputFiles = [] - for index, inputFile in enumerate(fileNames): - if totalMonths[index] > lastTotalMonths: - inputFiles.append(inputFile) + inputFiles = [ + inputFile for index, inputFile in enumerate(fileNames) + if totalMonths[index] > lastTotalMonths + ] if len(inputFiles) == 0: - # nothing to do - return + return append, inputFiles else: - # there is an output file but it has the wrong variables - # so we need ot delete it. - self.logger.warning('Warning: deleting file {self.outputFile}' - ' because it is empty or some variables' - ' were missing') + self.logger.warning( + f'Warning: deleting file {self.outputFile} because it ' + 'is empty or some variables were missing') os.remove(self.outputFile) - variableList = variable_list + ['xtime'] + return append, inputFiles + + def _compute_time_series_with_ncrcat(self, variable_list): + """ + Uses ncrcat to extract time series from conservationCheckOutput files. + + Raises + ------ + OSError + If ``ncrcat`` is not in the system path. + """ + if shutil.which('ncrcat') is None: + raise OSError('ncrcat not found. Make sure the latest nco ' + 'package is installed: \n' + 'conda install nco\n' + 'Note: this presumes use of the conda-forge ' + 'channel.') + + variable_list = variable_list + ['xtime'] + append, inputFiles = self._check_output_safe_to_append(variable_list) + + if len(inputFiles) == 0: + # nothing to do + return args = ['ncrcat', '-4', '--no_tmp_fl', - '-v', ','.join(variableList)] + '-v', ','.join(variable_list)] if append: args.append('--record_append') @@ -710,3 +723,67 @@ def _compute_time_series_with_ncrcat(self, variable_list): if process.returncode != 0: raise subprocess.CalledProcessError(process.returncode, ' '.join(args)) + + def _compute_time_series_with_xarray(self, variable_list): + """ + Uses xarray to extract time series from conservationCheckOutput files, + handling redundant `xtime` entries and sorting by `xtime`. + + Parameters + ---------- + variable_list : list of str + List of variables to include in the time series. + """ + # Authors + # ------- + # Xylar Asay-Davis + + inputFiles = self.inputFiles + variable_list = variable_list + ['xtime'] + + append, inputFiles = self._check_output_safe_to_append(variable_list) + + # Open all input files as a single dataset + self.logger.info( + f'Opening input files with xarray: {inputFiles[0]} ... ' + f'{inputFiles[-1]}') + ds = xr.open_mfdataset( + inputFiles, + combine='nested', + concat_dim='Time', + data_vars='minimal', + coords='minimal', + compat='override' + ) + + # Select only the requested variables + ds = ds[variable_list] + + # Handle redundant `xtime` entries by keeping the last occurrence + self.logger.info('Removing redundant xtime entries...') + _, unique_indices = np.unique(ds['xtime'].values, return_index=True) + unique_indices = sorted(unique_indices) # Ensure ascending order + ds = ds.isel(Time=unique_indices) + + if append: + # Load the existing dataset and combine it with the new dataset + self.logger.info( + f'Appending to existing dataset in {self.outputFile}...') + with xr.open_dataset(self.outputFile) as existing_ds: + ds = xr.concat([existing_ds, ds], dim='Time') + # Remove redundant `xtime` entries again after concatenation + _, unique_indices = np.unique( + ds['xtime'].values, return_index=True) + unique_indices = sorted(unique_indices) + ds = ds.isel(Time=unique_indices) + + # Sort by `xtime` to ensure the time series is in ascending order + self.logger.info('Sorting by xtime...') + ds = ds.sortby('xtime') + + # Save the resulting dataset to the output file + self.logger.info( + f'Saving concatenated dataset to {self.outputFile}...') + ds.to_netcdf(self.outputFile, format='NETCDF4') + + self.logger.info('Time series successfully created with xarray.') From 48e9c415f45e50118443010857635458e2867adc Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Fri, 4 Apr 2025 15:35:01 -0500 Subject: [PATCH 002/116] Remove unnecessary packages from dev spec --- dev-spec.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/dev-spec.txt b/dev-spec.txt index dfeb22413..ca9a1f888 100644 --- a/dev-spec.txt +++ b/dev-spec.txt @@ -32,8 +32,6 @@ shapely>=2.0,<3.0 xarray>=0.14.1 # Development -flake8 -git pip pytest From 5520c3ca8a15c73b1c6208810314c38624a24439 Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Fri, 4 Apr 2025 15:36:22 -0500 Subject: [PATCH 003/116] Fix optional deps in pyproject.toml --- pyproject.toml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d3d7f0d25..674be4f7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,24 @@ dependencies = [ "xarray>=0.14.1" ] +[project.optional-dependencies] +docs = [ + # building documentation + "mock", + "m2r2>=0.3.3", + "mistune<2", + "sphinx", + "sphinx_rtd_theme", + "sphinx-multiversion", + "tabulate", +] + +dev = [ + # linting and testing + "pip", + "pytest", +] + [build-system] requires = ["setuptools>=60"] build-backend = "setuptools.build_meta" From 338a52216dfd4f5601a5e1c7d55704e330e74eb4 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 18 Feb 2025 03:18:33 -0600 Subject: [PATCH 004/116] Add function compute_zinterface --- mpas_analysis/ocean/utility.py | 51 ++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/mpas_analysis/ocean/utility.py b/mpas_analysis/ocean/utility.py index a96c7c3fa..66f4ec299 100644 --- a/mpas_analysis/ocean/utility.py +++ b/mpas_analysis/ocean/utility.py @@ -126,3 +126,54 @@ def compute_zmid(bottomDepth, maxLevelCell, layerThickness): zMid = zLayerBot + 0.5 * layerThickness return zMid + + +def compute_zinterface(bottomDepth, maxLevelCell, layerThickness): + """ + Computes zInterface given data arrays for bottomDepth, maxLevelCell and + layerThickness + + Parameters + ---------- + bottomDepth : ``xarray.DataArray`` + the depth of the ocean bottom (positive) + + maxLevelCell : ``xarray.DataArray`` + the 0-based vertical index of the bottom of the ocean + + layerThickness : ``xarray.DataArray`` + the thickness of MPAS-Ocean layers (possibly as a function of time) + + Returns + ------- + zInterface : ``xarray.DataArray`` + the vertical coordinate defining the interfaces between layers, masked + below the bathymetry + """ + # Authors + # ------- + # Xylar Asay-Davis + + nVertLevels = layerThickness.sizes['nVertLevels'] + + vertIndex = \ + xarray.DataArray.from_dict({'dims': ('nVertLevels',), + 'data': numpy.arange(nVertLevels)}) + + layerThickness = layerThickness.where(vertIndex <= maxLevelCell) + thicknessSum = layerThickness.sum(dim='nVertLevels') + + zSurface = -bottomDepth + thicknessSum + + zInterfaceList = [zSurface] + + zTop = zSurface + + for zIndex in range(nVertLevels): + zBot = zTop - layerThickness.isel(nVertLevels=zIndex) + zInterfaceList.append(zBot) + zTop = zBot + + zInterface = xarray.concat(zInterfaceList, dim='nVertLevelsP1').transpose( + 'nCells', 'nVertLevelsP1') + return zInterface From babb4a922a3f7e83cbc9794ef0251535853dc009 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 18 Feb 2025 03:51:06 -0600 Subject: [PATCH 005/116] Update RemapDepthSlicesSubtask to support nVertLevelsP1 --- .../ocean/remap_depth_slices_subtask.py | 176 +++++++++++------- 1 file changed, 113 insertions(+), 63 deletions(-) diff --git a/mpas_analysis/ocean/remap_depth_slices_subtask.py b/mpas_analysis/ocean/remap_depth_slices_subtask.py index a3395916d..f145fcdb3 100644 --- a/mpas_analysis/ocean/remap_depth_slices_subtask.py +++ b/mpas_analysis/ocean/remap_depth_slices_subtask.py @@ -13,7 +13,10 @@ from mpas_analysis.shared.climatology import RemapMpasClimatologySubtask -from mpas_analysis.ocean.utility import compute_zmid +from mpas_analysis.ocean.utility import ( + compute_zinterface, + compute_zmid +) class RemapDepthSlicesSubtask(RemapMpasClimatologySubtask): @@ -27,11 +30,9 @@ class RemapDepthSlicesSubtask(RemapMpasClimatologySubtask): A list of depths at which the climatology will be sliced in the vertical. - maxLevelCell : xarray.DataArray - The vertical index of the bottom cell in MPAS results - - verticalIndices : xarray.DataArray - The vertical indices of slice to be plotted + dsSlice : xarray.Dataset + A dataset containing information needed to index variables at the + designated depths """ # Authors # ------- @@ -39,7 +40,7 @@ class RemapDepthSlicesSubtask(RemapMpasClimatologySubtask): def __init__(self, mpasClimatologyTask, parentTask, climatologyName, variableList, seasons, depths, comparisonGridNames=['latlon'], - iselValues=None): + iselValues=None, subtaskName='remapDepthSlices'): """ Construct the analysis task and adds it as a subtask of the @@ -78,18 +79,23 @@ def __init__(self, mpasClimatologyTask, parentTask, climatologyName, iselValues : dict, optional A dictionary of dimensions and indices (or ``None``) used to extract a slice of the MPAS field(s). + + subtaskName : str, optional + The name of the subtask """ # Authors # ------- # Xylar Asay-Davis self.depths = depths + self.dsSlice = xr.Dataset() # call the constructor from the base class # (RemapMpasClimatologySubtask) - super(RemapDepthSlicesSubtask, self).__init__( + super().__init__( mpasClimatologyTask, parentTask, climatologyName, variableList, - seasons, comparisonGridNames, iselValues) + seasons, comparisonGridNames, iselValues, + subtaskName=subtaskName) def run_task(self): """ @@ -110,66 +116,93 @@ def run_task(self): ds = ds[['maxLevelCell', 'bottomDepth', 'layerThickness']] ds = ds.isel(Time=0) - self.maxLevelCell = ds.maxLevelCell - 1 - depthNames = [str(depth) for depth in self.depths] - zMid = compute_zmid(ds.bottomDepth, ds.maxLevelCell-1, - ds.layerThickness) - ocean_mask = (ds.maxLevelCell > 0) + bottomDepth = ds.bottomDepth + layerThickness = ds.layerThickness + maxLevelCell = ds.maxLevelCell - 1 + self.dsSlice['maxLevelCell'] = maxLevelCell + + zMid = compute_zmid(bottomDepth, maxLevelCell, layerThickness) - nVertLevels = zMid.shape[1] + zInterface = compute_zinterface( + bottomDepth, maxLevelCell, layerThickness) + + horizontalMask = maxLevelCell >= 0 + + nVertLevels = ds.sizes['nVertLevels'] zMid.coords['verticalIndex'] = \ ('nVertLevels', np.arange(nVertLevels)) - zTop = zMid.isel(nVertLevels=0) + nVertLevelsP1 = zInterface.sizes['nVertLevelsP1'] + zInterface.coords['verticalIndex'] = \ + ('nVertLevelsP1', + np.arange(nVertLevelsP1)) + + zLevelTop = zMid.isel(nVertLevels=0) # Each vertical layer has at most one non-NaN value so the "sum" # over the vertical is used to collapse the array in the vertical # dimension - zBot = zMid.where(zMid.verticalIndex == self.maxLevelCell).sum( + zLevelBot = zMid.where(zMid.verticalIndex == maxLevelCell).sum( dim='nVertLevels') - verticalIndices = np.zeros((len(self.depths), ds.sizes['nCells']), int) + zInterfaceTop = zInterface.isel(nVertLevelsP1=0) + zInterfaceBot = zInterface.where( + zInterface.verticalIndex == maxLevelCell + 1).sum( + dim='nVertLevelsP1') - mask = np.zeros(verticalIndices.shape, bool) + levelIndices = np.zeros((len(self.depths), ds.sizes['nCells']), int) + levelMask = np.zeros(levelIndices.shape, bool) + interfaceIndices = np.zeros(levelIndices.shape, int) + interfaceMask = np.zeros(levelIndices.shape, bool) for depthIndex, depth in enumerate(self.depths): depth = self.depths[depthIndex] if depth == 'top': - # switch to zero-based index - verticalIndices[depthIndex, :] = 0 - mask[depthIndex, :] = self.maxLevelCell.values >= 0 + levelIndices[depthIndex, :] = 0 + levelMask[depthIndex, :] = horizontalMask.values + interfaceIndices[depthIndex, :] = 0 + interfaceMask[depthIndex, :] = horizontalMask.values elif depth == 'bot': # switch to zero-based index - verticalIndices[depthIndex, :] = self.maxLevelCell.values - mask[depthIndex, :] = self.maxLevelCell.values >= 0 + levelIndices[depthIndex, :] = maxLevelCell.values + levelMask[depthIndex, :] = horizontalMask.values + interfaceIndices[depthIndex, :] = maxLevelCell.values + 1 + interfaceMask[depthIndex, :] = horizontalMask.values else: - - diff = np.abs(zMid - depth).where(ocean_mask, drop=True) - verticalIndex = diff.argmin(dim='nVertLevels') - - verticalIndices[depthIndex, ocean_mask.values] = \ - verticalIndex.values - mask[depthIndex, :] = np.logical_and(depth <= zTop, - depth >= zBot).values - - self.verticalIndices = \ - xr.DataArray.from_dict({'dims': ('depthSlice', 'nCells'), - 'coords': {'depthSlice': - {'dims': ('depthSlice',), - 'data': depthNames}}, - 'data': verticalIndices}) - self.verticalIndexMask = \ - xr.DataArray.from_dict({'dims': ('depthSlice', 'nCells'), - 'coords': {'depthSlice': - {'dims': ('depthSlice',), - 'data': depthNames}}, - 'data': mask}) + levelDiff = np.abs(zMid - depth).where(horizontalMask, + drop=True) + levelIndex = levelDiff.argmin(dim='nVertLevels') + + levelIndices[depthIndex, horizontalMask.values] = \ + levelIndex.values + levelMask[depthIndex, :] = np.logical_and( + depth <= zLevelTop, depth >= zLevelBot).values + + interfaceDiff = np.abs(zInterface - depth).where( + horizontalMask, drop=True) + interfaceIndex = interfaceDiff.argmin(dim='nVertLevelsP1') + + interfaceIndices[depthIndex, horizontalMask.values] = \ + interfaceIndex.values + interfaceMask[depthIndex, :] = np.logical_and( + depth <= zInterfaceTop, depth >= zInterfaceBot).values + + self.dsSlice.coords['depthSlice'] = ('depthSlice', depthNames) + + self.dsSlice['levelIndices'] = (('depthSlice', 'nCells'), + levelIndices) + self.dsSlice['levelIndexMask'] = (('depthSlice', 'nCells'), + levelMask) + self.dsSlice['interfaceIndices'] = (('depthSlice', 'nCells'), + interfaceIndices) + self.dsSlice['interfaceIndexMask'] = (('depthSlice', 'nCells'), + interfaceMask) # then, call run from the base class (RemapMpasClimatologySubtask), # which will perform the main function of the task - super(RemapDepthSlicesSubtask, self).run_task() + super().run_task() def customize_masked_climatology(self, climatology, season): """ @@ -197,29 +230,46 @@ def customize_masked_climatology(self, climatology, season): if self.depths is None: return climatology - climatology.coords['verticalIndex'] = \ - ('nVertLevels', - np.arange(climatology.sizes['nVertLevels'])) + if 'nVertLevels' in climatology.dims: + climatology.coords['levelIndex'] = \ + ('nVertLevels', + np.arange(climatology.sizes['nVertLevels'])) + if 'nVertLevelsP1' in climatology.dims: + climatology.coords['interfaceIndex'] = \ + ('nVertLevelsP1', + np.arange(climatology.sizes['nVertLevelsP1'])) depthNames = [str(depth) for depth in self.depths] climatology.coords['depthSlice'] = ('depthSlice', depthNames) - for variableName in self.variableList: - if 'nVertLevels' not in climatology[variableName].dims: - continue + levelIndices = self.dsSlice.levelIndices + levelIndexMask = self.dsSlice.levelIndexMask + interfaceIndices = self.dsSlice.interfaceIndices + interfaceIndexMask = self.dsSlice.interfaceIndexMask - # mask only the values with the right vertical index - da = climatology[variableName].where( - climatology.verticalIndex == self.verticalIndices) - - # Each vertical layer has at most one non-NaN value so the "sum" - # over the vertical is used to collapse the array in the vertical - # dimension - climatology[variableName] = \ - da.sum(dim='nVertLevels').where(self.verticalIndexMask) - - climatology = climatology.drop_vars('verticalIndex') + for variableName in self.variableList: + if 'nVertLevels' in climatology[variableName].dims: + # mask only the values with the right vertical index + da = climatology[variableName].where( + climatology.levelIndex == levelIndices) + + # Each vertical layer has at most one non-NaN value so the + # "sum" over the vertical is used to collapse the array in the + # vertical dimension + climatology[variableName] = \ + da.sum(dim='nVertLevels').where(levelIndexMask) + elif 'nVertLevelsP1' in climatology[variableName].dims: + da = climatology[variableName].where( + climatology.interfaceIndex == interfaceIndices) + + climatology[variableName] = \ + da.sum(dim='nVertLevelsP1').where(interfaceIndexMask) + + if 'levelIndex' in climatology.coords: + climatology = climatology.drop_vars('levelIndex') + if 'interfaceIndex' in climatology.coords: + climatology = climatology.drop_vars('interfaceIndex') climatology = climatology.transpose('depthSlice', 'nCells') From 1f0ed1afaebdd653c2f61f9d92b640416cf4b648 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 18 Feb 2025 04:21:29 -0600 Subject: [PATCH 006/116] Add ClimatologyMapCustom This analysis task supports a custom list of variables on cells to be plotted at selected depths and on specified maps and over the desired seasons. --- mpas_analysis/__main__.py | 3 + mpas_analysis/default.cfg | 209 +++++++++++ mpas_analysis/ocean/__init__.py | 4 + mpas_analysis/ocean/climatology_map_custom.py | 341 ++++++++++++++++++ 4 files changed, 557 insertions(+) create mode 100644 mpas_analysis/ocean/climatology_map_custom.py diff --git a/mpas_analysis/__main__.py b/mpas_analysis/__main__.py index 4bbb7c88b..ea00d46e0 100644 --- a/mpas_analysis/__main__.py +++ b/mpas_analysis/__main__.py @@ -186,6 +186,9 @@ def build_analysis_list(config, controlConfig): config, oceanClimatologyTasks['avg'], oceanRegionMasksTask, controlConfig)) + analyses.append(ocean.ClimatologyMapCustom( + config, oceanClimatologyTasks['avg'], controlConfig)) + analyses.append(ocean.ConservationTask( config, controlConfig)) diff --git a/mpas_analysis/default.cfg b/mpas_analysis/default.cfg index 3fcd11d33..a949e8d47 100755 --- a/mpas_analysis/default.cfg +++ b/mpas_analysis/default.cfg @@ -2151,6 +2151,215 @@ normTypeDifference = linear normArgsDifference = {'vmin': -0.2, 'vmax': 0.2} +[climatologyMapCustom] +## options related to plotting climatology maps of any field at various depths +## (if they include a depth dimension) without observatons for comparison + +# comparison grid(s) +comparisonGrids = ['latlon'] + +# Months or seasons to plot (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, +# Nov, Dec, JFM, AMJ, JAS, OND, ANN) +seasons = ['ANN'] + +# list of depths in meters (positive up) at which to analyze, 'top' for the +# sea surface, 'bot' for the sea floor +depths = ['top', -200, -400, -600, -800, -1000, -1500, -2000, 'bot'] + +# a list of variables available to plot. New variables can be added as long +# as they correspond to a single field already found in MPAS-Ocean's +# timeSeriesStatsMonthly output. Add the 'name', 'title', 'units' (with $$ +# instead a single dollar sign for the config parser), and 'mpas'(the +# timeSeriesStatsMonthly variable name as a single-item list) entries for each +# variable. Then, add a section below climatologyMapCustom with +# the colormap settings for that variable. +availableVariables = { + 'temperature': + {'title': 'Potential Temperature', + 'units': r'$$^\circ$$C', + 'mpas': ['timeMonthly_avg_activeTracers_temperature']}, + 'salinity': + {'title': 'Salinity', + 'units': 'PSU', + 'mpas': ['timeMonthly_avg_activeTracers_salinity']}, + 'potentialDensity': + {'title': 'Potential Density', + 'units': 'kg m$$^{-3}$$', + 'mpas': ['timeMonthly_avg_potentialDensity']}, + 'thermalForcing': + {'title': 'Thermal Forcing', + 'units': r'$$^\circ$$C', + 'mpas': ['timeMonthly_avg_activeTracers_temperature', + 'timeMonthly_avg_activeTracers_salinity', + 'timeMonthly_avg_density', + 'timeMonthly_avg_activeTracers_layerThickness']}, + 'zonalVelocity': + {'title': 'Zonal Velocity', + 'units': r'm s$$^{-1}$$', + 'mpas': ['timeMonthly_avg_velocityZonal']}, + 'meridionalVelocity': + {'title': 'Meridional Velocity', + 'units': r'm s$$^{-1}$$', + 'mpas': ['timeMonthly_avg_velocityMeridional']}, + 'velocityMagnitude': + {'title': 'Zonal Velocity', + 'units': r'm s$$^{-1}$$', + 'mpas': ['timeMonthly_avg_velocityZonal', + 'timeMonthly_avg_velocityMeridional']}, + 'vertVelocity': + {'title': 'Vertical Velocity', + 'units': r'm s$$^{-1}$$', + 'mpas': ['timeMonthly_avg_vertVelocityTop']}, + 'vertDiff': + {'title': 'Vertical Diffusivity', + 'units': r'm s$$^{-1}$$', + 'mpas': ['timeMonthly_avg_vertDiffTopOfCell']}, + 'vertVisc': + {'title': 'Vertical Viscosity', + 'units': r'm s$$^{-1}$$', + 'mpas': ['timeMonthly_avg_vertViscTopOfCell']}, + 'mixedLayerDepth': + {'title': 'Mixed Layer Depth', + 'units': 'm', + 'mpas': ['timeMonthly_avg_dThreshMLD'], + 'has_depth': False}, + } + +# a list of fields top plot for each transect. All supported fields are listed +# below +variables = [] + + +[climatologyMapCustomTemperature] +## options related to plotting climatology maps of potential temperature at +## various levels, including the sea surface and sea floor, possibly against +### control model results + +# colormap for model/observations +colormapNameResult = RdYlBu_r +# whether the colormap is indexed or continuous +colormapTypeResult = continuous +# the type of norm used in the colormap +normTypeResult = linear +# A dictionary with keywords for the norm +normArgsResult = {'vmin': -2., 'vmax': 10.} +# place the ticks automatically by default +# colorbarTicksResult = numpy.linspace(-2., 10., 9) + +# colormap for differences +colormapNameDifference = balance +# whether the colormap is indexed or continuous +colormapTypeDifference = continuous +# the type of norm used in the colormap +normTypeDifference = linear +# A dictionary with keywords for the norm +normArgsDifference = {'vmin': -5., 'vmax': 5.} +# place the ticks automatically by default +# colorbarTicksDifference = numpy.linspace(-5., 5., 9) + +[climatologyMapCustomSalinity] +colormapNameResult = haline +colormapTypeResult = continuous +normTypeResult = linear +normArgsResult = {'vmin': 32.2, 'vmax': 35.5} +colormapNameDifference = balance +colormapTypeDifference = continuous +normTypeDifference = linear +normArgsDifference = {'vmin': -1.5, 'vmax': 1.5} + +[climatologyMapCustomPotentialDensity] +colormapNameResult = Spectral_r +colormapTypeResult = continuous +normTypeResult = linear +normArgsResult = {'vmin': 1026.5, 'vmax': 1028.} +colormapNameDifference = balance +colormapTypeDifference = continuous +normTypeDifference = linear +normArgsDifference = {'vmin': -0.3, 'vmax': 0.3} + +[climatologyMapCustomThermalForcing] +colormapNameResult = thermal +colormapTypeResult = continuous +normTypeResult = linear +normArgsResult = {'vmin': -1., 'vmax': 5.} +colormapNameDifference = balance +colormapTypeDifference = continuous +normTypeDifference = linear +normArgsDifference = {'vmin': -3., 'vmax': 3.} + +[climatologyMapCustomZonalVelocity] +colormapNameResult = delta +colormapTypeResult = continuous +normTypeResult = linear +normArgsResult = {'vmin': -0.2, 'vmax': 0.2} +colormapNameDifference = balance +colormapTypeDifference = continuous +normTypeDifference = linear +normArgsDifference = {'vmin': -0.2, 'vmax': 0.2} + +[climatologyMapCustomMeridionalVelocity] +colormapNameResult = delta +colormapTypeResult = continuous +normTypeResult = linear +normArgsResult = {'vmin': -0.2, 'vmax': 0.2} +colormapNameDifference = balance +colormapTypeDifference = continuous +normTypeDifference = linear +normArgsDifference = {'vmin': -0.2, 'vmax': 0.2} + +[climatologyMapCustomVelocityMagnitude] +colormapNameResult = ice +colormapTypeResult = continuous +normTypeResult = log +normArgsResult = {'vmin': 1.e-3, 'vmax': 1.} +colormapNameDifference = balance +colormapTypeDifference = continuous +normTypeDifference = linear +normArgsDifference = {'vmin': -0.2, 'vmax': 0.2} + +[climatologyMapCustomVertVelocity] +colormapNameResult = delta +colormapTypeResult = continuous +normTypeResult = linear +normArgsResult = {'vmin': -1e-5, 'vmax': 1e-5} +colormapNameDifference = balance +colormapTypeDifference = continuous +normTypeDifference = linear +normArgsDifference = {'vmin': -1e-5, 'vmax': 1e-5} + +[climatologyMapCustomVertDiff] +colormapNameResult = rain +colormapTypeResult = continuous +normTypeResult = log +normArgsResult = {'vmin': 1e-6, 'vmax': 1.} +colormapNameDifference = balance +colormapTypeDifference = continuous +normTypeDifference = linear +normArgsDifference = {'vmin': -0.5, 'vmax': 0.5} + +[climatologyMapCustomVertVisc] +colormapNameResult = rain +colormapTypeResult = continuous +normTypeResult = log +normArgsResult = {'vmin': 1e-6, 'vmax': 1.} +colormapNameDifference = balance +colormapTypeDifference = continuous +normTypeDifference = linear +normArgsDifference = {'vmin': -0.5, 'vmax': 0.5} + +[climatologyMapCustomMixedLayerDepth] +colormapNameResult = viridis +colormapTypeResult = continuous +normTypeResult = log +normArgsResult = {'vmin': 10., 'vmax': 300.} +colorbarTicksResult = [10, 20, 40, 60, 80, 100, 200, 300] +colormapNameDifference = balance +colormapTypeDifference = continuous +normTypeDifference = symLog +normArgsDifference = {'linthresh': 10., 'linscale': 0.5, 'vmin': -200., + 'vmax': 200.} +colorbarTicksDifference = [-200., -100., -50., -20., -10., 0., 10., 20., 50., 100., 200.] + [climatologyMapSose] ## options related to plotting climatology maps of Antarctic fields at various ## levels, including the sea floor against control model results and SOSE diff --git a/mpas_analysis/ocean/__init__.py b/mpas_analysis/ocean/__init__.py index 107d0785d..c99feb4b4 100644 --- a/mpas_analysis/ocean/__init__.py +++ b/mpas_analysis/ocean/__init__.py @@ -25,6 +25,10 @@ from mpas_analysis.ocean.climatology_map_argo import \ ClimatologyMapArgoTemperature, ClimatologyMapArgoSalinity +from mpas_analysis.ocean.climatology_map_custom import ( + ClimatologyMapCustom +) + from mpas_analysis.ocean.conservation import ConservationTask from mpas_analysis.ocean.time_series_temperature_anomaly import \ diff --git a/mpas_analysis/ocean/climatology_map_custom.py b/mpas_analysis/ocean/climatology_map_custom.py new file mode 100644 index 000000000..d714ccbb6 --- /dev/null +++ b/mpas_analysis/ocean/climatology_map_custom.py @@ -0,0 +1,341 @@ +# This software is open source software available under the BSD-3 license. +# +# Copyright (c) 2022 Triad National Security, LLC. All rights reserved. +# Copyright (c) 2022 Lawrence Livermore National Security, LLC. All rights +# reserved. +# Copyright (c) 2022 UT-Battelle, LLC. All rights reserved. +# +# Additional copyright and license information can be found in the LICENSE file +# distributed with this code, or at +# https://raw.githubusercontent.com/MPAS-Dev/MPAS-Analysis/main/LICENSE + +import numpy as np +from mpas_tools.cime.constants import constants as cime_constants + +from mpas_analysis.ocean.remap_depth_slices_subtask import ( + RemapDepthSlicesSubtask +) +from mpas_analysis.shared import AnalysisTask +from mpas_analysis.shared.climatology import RemapMpasClimatologySubtask +from mpas_analysis.shared.plot import PlotClimatologyMapSubtask + + +class ClimatologyMapCustom(AnalysisTask): + """ + A felxible analysis task for plotting climatologies of any MPAS-Ocean field + on cells from timeSeriesStatsMonthly at various depths (if the field has + vertical levels) and for various seasons. Various derived fields are also + supported: + * velocity magnitude + * thermal forcing (temperature - freezing temperature) + """ + + def __init__(self, config, mpasClimatologyTask, controlConfig=None): + """ + Construct the analysis task. + + Parameters + ---------- + config : mpas_tools.config.MpasConfigParser + Configuration options + + mpasClimatologyTask : ``MpasClimatologyTask`` + The task that produced the climatology to be remapped and plotted + + controlconfig : mpas_tools.config.MpasConfigParser, optional + Configuration options for a control run (if any) + """ + + taskName = 'climatologyMapCustom' + + sectionName = taskName + variablesNames = config.getexpression(sectionName, 'variables') + + tags = ['climatology', 'horizontalMap'] + variablesNames + + # call the constructor from the base class (AnalysisTask) + super().__init__( + config=config, taskName=taskName, componentName='ocean', tags=tags) + + if len(variablesNames) == 0: + return + + variablesNames = config.getexpression(sectionName, 'variables') + if len(variablesNames) == 0: + return + + availableVariables = config.getexpression(sectionName, + 'availableVariables') + + variables = {varName: availableVariables[varName] for varName in + variablesNames} + + for varName in variablesNames: + if 'has_depth' not in variables[varName]: + # we assume variables have depth unless otherwise specified + variables[varName]['has_depth'] = True + + variables3D = {varName: variables[varName] for varName in + variables if variables[varName]['has_depth']} + + variableList2D = [variables[varName]['mpas'][0] for varName in + variables if not variables[varName]['has_depth']] + + # read in what seasons we want to plot + seasons = config.getexpression(sectionName, 'seasons') + + if len(seasons) == 0: + raise ValueError(f'config section {sectionName} does not contain ' + 'valid list of seasons') + + comparisonGridNames = config.getexpression(sectionName, + 'comparisonGrids') + + if len(comparisonGridNames) == 0: + raise ValueError(f'config section {sectionName} does not contain ' + 'valid list of comparison grids') + + depths = config.getexpression(sectionName, 'depths') + + if len(depths) == 0: + raise ValueError(f'config section {sectionName} does not ' + f'contain valid list of depths') + + if variables3D: + remapMpasSubtask3D = RemapMpasDerivedVariableClimatology( + mpasClimatologyTask=mpasClimatologyTask, + parentTask=self, + climatologyName='custom3D', + variables=variables3D, + seasons=seasons, + depths=depths, + comparisonGridNames=comparisonGridNames) + else: + remapMpasSubtask3D = None + + if len(variableList2D) > 0: + remapMpasSubtask2D = RemapMpasClimatologySubtask( + mpasClimatologyTask=mpasClimatologyTask, + parentTask=self, + climatologyName='custom2D', + variableList=variableList2D, + seasons=seasons, + comparisonGridNames=comparisonGridNames, + subtaskName='remap2DVariables') + else: + remapMpasSubtask2D = None + + galleryGroup = 'Custom Climatology Maps' + groupLink = 'custclimmaps' + + for varName, metadata in variables.items(): + title = metadata['title'] + units = metadata['units'] + hasDepth = metadata['has_depth'] + upperVarName = varName[0].upper() + varName[1:] + varSectionName = f'{self.taskName}{upperVarName}' + + remapObsSubtask = None + + refTitleLabel = None + diffTitleLabel = None + if controlConfig is not None: + controlRunName = controlConfig.get('runs', 'mainRunName') + refTitleLabel = f'Control: {controlRunName}' + diffTitleLabel = 'Main - Control' + + if hasDepth: + localDepths = depths + remapMpasSubtask = remapMpasSubtask3D + mpasVarName = varName + else: + localDepths = [None] + remapMpasSubtask = remapMpasSubtask2D + mpasVarName = metadata['mpas'][0] + + for comparisonGridName in comparisonGridNames: + for depth in localDepths: + for season in seasons: + + subtaskName = f'plot{upperVarName}_{season}_' \ + f'{comparisonGridName}' + if depth is not None: + subtaskName = f'{subtaskName}_depth_{depth}' + + subtask = PlotClimatologyMapSubtask( + parentTask=self, + season=season, + comparisonGridName=comparisonGridName, + remapMpasClimatologySubtask=remapMpasSubtask, + remapObsClimatologySubtask=remapObsSubtask, + controlConfig=controlConfig, + depth=depth, + subtaskName=subtaskName) + + subtask.set_plot_info( + outFileLabel=varName, + fieldNameInTitle=title, + mpasFieldName=mpasVarName, + refFieldName=mpasVarName, + refTitleLabel=refTitleLabel, + diffTitleLabel=diffTitleLabel, + unitsLabel=units, + imageCaption=title, + galleryGroup=galleryGroup, + groupSubtitle=None, + groupLink=groupLink, + galleryName=title, + configSectionName=varSectionName) + + self.add_subtask(subtask) + + +class RemapMpasDerivedVariableClimatology(RemapDepthSlicesSubtask): + """ + A subtask for computing derived variables (such as velocity magnitude and + thermal forcing) as part of remapping climatologies at depth slices + + Attributes + ---------- + variables : dict of dict + A dictionary of variable definitions, with variable names as keys + + """ + + def __init__(self, mpasClimatologyTask, parentTask, climatologyName, + variables, seasons, depths, comparisonGridNames): + + """ + Construct the analysis task and adds it as a subtask of the + ``parentTask``. + + Parameters + ---------- + mpasClimatologyTask : MpasClimatologyTask + The task that produced the climatology to be remapped + + parentTask : AnalysisTask + The parent task, used to get the ``taskName``, ``config`` and + ``componentName`` + + climatologyName : str + A name that describes the climatology (e.g. a short version of + the important field(s) in the climatology) used to name the + subdirectories for each stage of the climatology + + variables : dict of dict + A dictionary of variable definitions, with variable names as keys + + seasons : list of str, optional + A list of seasons (keys in ``shared.constants.monthDictionary``) + to be computed or ['none'] (not ``None``) if only monthly + climatologies are needed. + + depths : list of {None, float, 'top', 'bot'} + A list of depths at which the climatology will be sliced in the + vertical. + + comparisonGridNames : list of {'latlon', 'antarctic'}, optional + The name(s) of the comparison grid to use for remapping. + """ + self.variables = variables + + mpasVariables = set() + for variable in variables.values(): + for mpasVariable in variable['mpas']: + mpasVariables.add(mpasVariable) + + # call the constructor from the base class + # (RemapMpasClimatologySubtask) + super().__init__( + mpasClimatologyTask, parentTask, climatologyName, mpasVariables, + seasons, depths, comparisonGridNames, iselValues=None) + + def customize_masked_climatology(self, climatology, season): + """ + Construct velocity magnitude as part of the climatology + + Parameters + ---------- + climatology : ``xarray.Dataset`` object + the climatology data set + + season : str + The name of the season to be masked + + Returns + ------- + climatology : ``xarray.Dataset`` object + the modified climatology data set + """ + + # first, call the base class's version of this function so we extract + # the desired slices. + climatology = super().customize_masked_climatology(climatology, + season) + derivedVars = [] + self._add_vel_mag(climatology, derivedVars) + self._add_thermal_forcing(climatology, derivedVars) + + for varName, variable in self.variables.items(): + if varName not in derivedVars: + # rename variables from MPAS names to shorter names + mpasvarNames = variable['mpas'] + if len(mpasvarNames) == 1: + mpasvarName = mpasvarNames[0] + climatology[varName] = climatology[mpasvarName] + climatology.drop_vars(mpasvarName) + + climatology[varName].attrs['units'] = variable['units'] + climatology[varName].attrs['description'] = variable['title'] + + return climatology + + def _add_vel_mag(self, climatology, derivedVars): + """ + Add the velocity magnitude to the climatology if requested + """ + variables = self.variables + + varName = 'verlocityMagnitude' + if varName not in variables: + return + + derivedVars.append(varName) + + zonalVel = climatology.timeMonthly_avg_velocityZonal + meridVel = climatology.timeMonthly_avg_velocityMeridional + climatology[varName] = np.sqrt(zonalVel**2 + meridVel**2) + + def _add_thermal_forcing(self, climatology, derivedVars): + """ + Add thermal forcing to the climatology if requested + """ + variables = self.variables + + varName = 'thermalForcing' + if varName not in variables: + return + + derivedVars.append(varName) + + c0 = self.namelist.getfloat( + 'config_land_ice_cavity_freezing_temperature_coeff_0') + cs = self.namelist.getfloat( + 'config_land_ice_cavity_freezing_temperature_coeff_S') + cp = self.namelist.getfloat( + 'config_land_ice_cavity_freezing_temperature_coeff_p') + cps = self.namelist.getfloat( + 'config_land_ice_cavity_freezing_temperature_coeff_pS') + + temp = climatology.timeMonthly_avg_activeTracers_temperature + salin = climatology.timeMonthly_avg_activeTracers_salinity + dens = climatology.timeMonthly_avg_density + thick = climatology.timeMonthly_avg_layerThickness + + dp = cime_constants['SHR_CONST_G']*dens*thick + press = dp.cumsum(dim='nVertLevels') - 0.5*dp + + tempFreeze = c0 + cs*salin + cp*press + cps*press*salin + + climatology[varName] = temp - tempFreeze From c03c9864658883f6d8eb485b9c9ee18b62fafab9 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 19 Feb 2025 07:59:55 -0600 Subject: [PATCH 007/116] Add docs for climatologyMapCustom --- docs/developers_guide/api.rst | 1 + docs/users_guide/analysis_tasks.rst | 1 + .../tasks/climatologyMapCustom.rst | 260 ++++++++++++++++++ .../tasks/examples/clim_map_custom.png | Bin 0 -> 148593 bytes 4 files changed, 262 insertions(+) create mode 100644 docs/users_guide/tasks/climatologyMapCustom.rst create mode 100644 docs/users_guide/tasks/examples/clim_map_custom.png diff --git a/docs/developers_guide/api.rst b/docs/developers_guide/api.rst index c6cb11fd6..659916754 100644 --- a/docs/developers_guide/api.rst +++ b/docs/developers_guide/api.rst @@ -77,6 +77,7 @@ Ocean tasks ClimatologyMapArgoTemperature ClimatologyMapArgoSalinity ClimatologyMapWaves + ClimatologyMapCustom IndexNino34 MeridionalHeatTransport OceanHistogram diff --git a/docs/users_guide/analysis_tasks.rst b/docs/users_guide/analysis_tasks.rst index d5f2c83c9..d2973b43d 100644 --- a/docs/users_guide/analysis_tasks.rst +++ b/docs/users_guide/analysis_tasks.rst @@ -9,6 +9,7 @@ Analysis Tasks tasks/climatologyMapArgoTemperature tasks/climatologyMapBGC tasks/climatologyMapBSF + tasks/climatologyMapCustom tasks/climatologyMapEKE tasks/climatologyMapHeatFluxes tasks/climatologyMapMassFluxes diff --git a/docs/users_guide/tasks/climatologyMapCustom.rst b/docs/users_guide/tasks/climatologyMapCustom.rst new file mode 100644 index 000000000..98f33cc23 --- /dev/null +++ b/docs/users_guide/tasks/climatologyMapCustom.rst @@ -0,0 +1,260 @@ +.. _task_climatologyMapCustom: + +climatologyMapCustom +==================== + +An analysis task for plotting custom climatologies at various depths. This task +can plot both 2D and 3D variables on cells, the latter with both +``nVertlevels`` -- layer centers -- or ``nVertLevelsP1`` -- layer interfaces +-- as the vertical dimension. The task is designed to be highly coustomizable +via config sections and options, as described below. + +Component and Tags:: + + component: ocean + tags: climatology, horizontalMap + +Configuration Options +--------------------- + +The following configuration options are available for this task:: + + [climatologyMapCustom] + ## options related to plotting climatology maps of any field at various depths + ## (if they include a depth dimension) without observatons for comparison + + # comparison grid(s) + comparisonGrids = ['latlon'] + + # Months or seasons to plot (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, + # Nov, Dec, JFM, AMJ, JAS, OND, ANN) + seasons = ['ANN'] + + # list of depths in meters (positive up) at which to analyze, 'top' for the + # sea surface, 'bot' for the sea floor + depths = ['top', -200, -400, -600, -800, -1000, -1500, -2000, 'bot'] + + # a list of variables available to plot. New variables can be added as long + # as they correspond to a single field already found in MPAS-Ocean's + # timeSeriesStatsMonthly output. Add the 'name', 'title', 'units' (with $$ + # instead a single dollar sign for the config parser), and 'mpas'(the + # timeSeriesStatsMonthly variable name as a single-item list) entries for each + # variable. Then, add a section below climatologyMapCustom with + # the colormap settings for that variable. + availableVariables = { + 'temperature': + {'title': 'Potential Temperature', + 'units': r'$$^\circ$$C', + 'mpas': ['timeMonthly_avg_activeTracers_temperature']}, + 'salinity': + {'title': 'Salinity', + 'units': 'PSU', + 'mpas': ['timeMonthly_avg_activeTracers_salinity']}, + 'potentialDensity': + {'title': 'Potential Density', + 'units': 'kg m$$^{-3}$$', + 'mpas': ['timeMonthly_avg_potentialDensity']}, + 'thermalForcing': + {'title': 'Thermal Forcing', + 'units': r'$$^\circ$$C', + 'mpas': ['timeMonthly_avg_activeTracers_temperature', + 'timeMonthly_avg_activeTracers_salinity', + 'timeMonthly_avg_density', + 'timeMonthly_avg_activeTracers_layerThickness']}, + 'zonalVelocity': + {'title': 'Zonal Velocity', + 'units': r'm s$$^{-1}$$', + 'mpas': ['timeMonthly_avg_velocityZonal']}, + 'meridionalVelocity': + {'title': 'Meridional Velocity', + 'units': r'm s$$^{-1}$$', + 'mpas': ['timeMonthly_avg_velocityMeridional']}, + 'velocityMagnitude': + {'title': 'Zonal Velocity', + 'units': r'm s$$^{-1}$$', + 'mpas': ['timeMonthly_avg_velocityZonal', + 'timeMonthly_avg_velocityMeridional']}, + 'vertVelocity': + {'title': 'Vertical Velocity', + 'units': r'm s$$^{-1}$$', + 'mpas': ['timeMonthly_avg_vertVelocityTop']}, + 'vertDiff': + {'title': 'Vertical Diffusivity', + 'units': r'm s$$^{-1}$$', + 'mpas': ['timeMonthly_avg_vertDiffTopOfCell']}, + 'vertVisc': + {'title': 'Vertical Viscosity', + 'units': r'm s$$^{-1}$$', + 'mpas': ['timeMonthly_avg_vertViscTopOfCell']}, + 'mixedLayerDepth': + {'title': 'Mixed Layer Depth', + 'units': 'm', + 'mpas': ['timeMonthly_avg_dThreshMLD']}, + } + + # a list of fields top plot for each transect. All supported fields are listed + # below + variables = [] + + + [climatologyMapCustomTemperature] + ## options related to plotting climatology maps of potential temperature at + ## various levels, including the sea surface and sea floor, possibly against + ### control model results + + # colormap for model/observations + colormapNameResult = RdYlBu_r + # whether the colormap is indexed or continuous + colormapTypeResult = continuous + # the type of norm used in the colormap + normTypeResult = linear + # A dictionary with keywords for the norm + normArgsResult = {'vmin': -2., 'vmax': 10.} + # place the ticks automatically by default + # colorbarTicksResult = numpy.linspace(-2., 10., 9) + + # colormap for differences + colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = continuous + # the type of norm used in the colormap + normTypeDifference = linear + # A dictionary with keywords for the norm + normArgsDifference = {'vmin': -5., 'vmax': 5.} + # place the ticks automatically by default + # colorbarTicksDifference = numpy.linspace(-5., 5., 9) + + [climatologyMapCustomSalinity] + colormapNameResult = haline + colormapTypeResult = continuous + normTypeResult = linear + normArgsResult = {'vmin': 32.2, 'vmax': 35.5} + colormapNameDifference = balance + colormapTypeDifference = continuous + normTypeDifference = linear + normArgsDifference = {'vmin': -1.5, 'vmax': 1.5} + + [climatologyMapCustomPotentialDensity] + colormapNameResult = Spectral_r + colormapTypeResult = continuous + normTypeResult = linear + normArgsResult = {'vmin': 1026.5, 'vmax': 1028.} + colormapNameDifference = balance + colormapTypeDifference = continuous + normTypeDifference = linear + normArgsDifference = {'vmin': -0.3, 'vmax': 0.3} + + [climatologyMapCustomThermalForcing] + colormapNameResult = thermal + colormapTypeResult = continuous + normTypeResult = linear + normArgsResult = {'vmin': -1., 'vmax': 5.} + colormapNameDifference = balance + colormapTypeDifference = continuous + normTypeDifference = linear + normArgsDifference = {'vmin': -3., 'vmax': 3.} + + [climatologyMapCustomZonalVelocity] + colormapNameResult = delta + colormapTypeResult = continuous + normTypeResult = linear + normArgsResult = {'vmin': -0.2, 'vmax': 0.2} + colormapNameDifference = balance + colormapTypeDifference = continuous + normTypeDifference = linear + normArgsDifference = {'vmin': -0.2, 'vmax': 0.2} + + [climatologyMapCustomMeridionalVelocity] + colormapNameResult = delta + colormapTypeResult = continuous + normTypeResult = linear + normArgsResult = {'vmin': -0.2, 'vmax': 0.2} + colormapNameDifference = balance + colormapTypeDifference = continuous + normTypeDifference = linear + normArgsDifference = {'vmin': -0.2, 'vmax': 0.2} + + [climatologyMapCustomVelocityMagnitude] + colormapNameResult = ice + colormapTypeResult = continuous + normTypeResult = log + normArgsResult = {'vmin': 1.e-3, 'vmax': 1.} + colormapNameDifference = balance + colormapTypeDifference = continuous + normTypeDifference = linear + normArgsDifference = {'vmin': -0.2, 'vmax': 0.2} + + [climatologyMapCustomVertVelocity] + colormapNameResult = delta + colormapTypeResult = continuous + normTypeResult = linear + normArgsResult = {'vmin': -1e-5, 'vmax': 1e-5} + colormapNameDifference = balance + colormapTypeDifference = continuous + normTypeDifference = linear + normArgsDifference = {'vmin': -1e-5, 'vmax': 1e-5} + + [climatologyMapCustomVertDiff] + colormapNameResult = rain + colormapTypeResult = continuous + normTypeResult = log + normArgsResult = {'vmin': 1e-6, 'vmax': 1.} + colormapNameDifference = balance + colormapTypeDifference = continuous + normTypeDifference = linear + normArgsDifference = {'vmin': -0.5, 'vmax': 0.5} + + [climatologyMapCustomVertVisc] + colormapNameResult = rain + colormapTypeResult = continuous + normTypeResult = log + normArgsResult = {'vmin': 1e-6, 'vmax': 1.} + colormapNameDifference = balance + colormapTypeDifference = continuous + normTypeDifference = linear + normArgsDifference = {'vmin': -0.5, 'vmax': 0.5} + + [climatologyMapCustomMixedLayerDepth] + colormapNameResult = viridis + colormapTypeResult = continuous + normTypeResult = log + normArgsResult = {'vmin': 10., 'vmax': 300.} + colorbarTicksResult = [10, 20, 40, 60, 80, 100, 200, 300] + colormapNameDifference = balance + colormapTypeDifference = continuous + normTypeDifference = symLog + normArgsDifference = {'linthresh': 10., 'linscale': 0.5, 'vmin': -200., + 'vmax': 200.} + colorbarTicksDifference = [-200., -100., -50., -20., -10., 0., 10., 20., 50., 100., 200.] + + +There is a section for options that apply to all custom climatology maps and +one each for any available variables to plot. + +The option ``availableVariables`` is a dictionary with the names of the +variables available to plot as keys and dictionaries with the title, units, +and MPAS variable name(s) as values. New entries can be added as long as they +correspond to a single field already found in MPAS-Ocean's +``timeSeriesStatsMonthly`` output. For each variable, a section with the name +``climatologyMapCustom`` should be added with the colormap +settings for that variable, see :ref:`config_colormaps` for details. + +The option ``depths`` is a list of (approximate) depths at which to sample +the potential temperature field. A value of ``'top'`` indicates the sea +surface (or the ice-ocean interface under ice shelves) while a value of +``'bot'`` indicates the seafloor. + +By default, no fields are plotted. A user can select which fields to plot by +adding the desired field names to the ``variables`` list. + +For more details, see: + * :ref:`config_colormaps` + * :ref:`config_seasons` + * :ref:`config_comparison_grids` + +Example Result +-------------- + +.. image:: examples/clim_map_custom.png + :width: 500 px + :align: center diff --git a/docs/users_guide/tasks/examples/clim_map_custom.png b/docs/users_guide/tasks/examples/clim_map_custom.png new file mode 100644 index 0000000000000000000000000000000000000000..2eb60063de9a1100a41c4a782fc598565cfeee84 GIT binary patch literal 148593 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

_P-UQq;6~)Me4VxMgv>CVp3e6ys zKpMX;{M_|W1g%0{^vzh`T-Zwf9${>#@qeECJ(otX^NC(P>A0%;krqBy&2P~|2s78^ zUu;hazevvZJkFk2_lfzlYt6k91H7Rlt-7eNN;uVjq+-*LKax#n<~!qQ795LMtAc@z z#548-F%d@RIjJWh+$<;X2?*M2egdvSq+Z}SOaQfIDd3_F9I0Y_UrNE$0X)#4ssNHU zfY;mCBczIe0s;mN@H^URk|0L{-cpbhfj2SmB?8$*RnVrjQr+b8Kk8HEcrCRwhc6{8$;|Pg0&Nvx%;5R7X|W4 z2e$DZ5R}RQK2_kB11JqJh{%9331FiJm6N=MW`C@J)h?J!z?KaJt7jG#27q#?tY-%C z5(9;t!*Q*H!$ZmPQ!emvUI4rVd5IZtxX?!RK+q-NB0KH=1&Fa=8oXIwBT|wZZr3uD zqKh*gv50zGTIX|W0SgOPp{vnVSg(fm?3a%qDl^}WL3r}l_-}zJV=iZ$)XHAdHKyJ& zp<1p4GGb*t%MU7GwGfp#j6B@KHc~+ae#IU+P<6K#N~=>7^BD zt;citI!{dR$F03F8t@u$STak|26WojSX19!2(R}%-d%!ceIj7xc$0Cppm)G9SxCBo z8|JW)GU399URqrWPOv;qg1+VYaj=)kfq}t zBW}`slBBZMi>b&&d8!bO{=}JuwC45ECMzr@33x4XuWP^J)}3l#hT7Krc(0BIPgctq z4f$|E_7F62PR9fdK@#NcZgk+8gv=P*2b|!eP=L~HaUcTAvJvgWxB*(6Cjk5Cz7*&L zbMhrP1k?d?47`>A>1e<5a-yuhF@p2Np%)t5$H;W}LsC;#4Kgq8j2-`h&cZ1PqGU(Mc`JuQI4l zt&_j}pgv8Lw#IdVqVGvDJ5;f{oomfHVNKhQcN3&@E;f_DL{sMM_erth~u>?|`5rZTx;UEX|p&0Zo-fSf;RFb$RUVqw$w^o4HYW(WSE z);5d`jvx31HUiFHfk9CvIQS^yqfU@0C252eVq3dZSH#_Te%#{4NJ=c$_KX*UT2JW^K9v$qA9C^;TAO^(WTgLzPgPHc+3cM2)3yAM= zHTJ23o)mX~g*BEDWg?3-3ukg`>L|=>OChW4{Iqjy)3cu}e;6P6$_++Y)#-ChjlaHT zzSlE<)P6u-3||J#xkiu=R47x@HZ9bEZX7#s|be3ox*5lh!YUR``(^)`&^bP?K$aTWH$=}@cZFyqS4O zi;_t;*}zZs8nv~tloA1ficC9&sDO__EZ4ZUx+R`?)>Mfxf=ucWHUUWw#g+i>)-*d* zCKj_mt&qyjjgF0iFH$BV`{$+Whd7@oJ>mMS&d$ylt$zfjZXH54VJQH0Qhf~Q5-Q;9 z$oFLr#9wz^^B#WlW4nO=zwpzcG6Yl$mTnxN`{;Ss=s6T}qV0~R&#tVDf_Yjxj?fq= z+m?@b(w_6+#saB40P^Hb=Zzr5ehHjmqX68YeAf=lMj>dHoLAmekaY;SKU~Uk4q^O- zF<4y3K0B>3uEsgmnBz+>1&<1&iHxqo zm2>s#P=kcHbGvwdsn|Zb^~=X~5#fh$bi$~WzH^yY>?J>E+(eN-vf?w)v;S)H+>}YQ znD1C*4#bQ%dqOQ4b6*B&+!oz|_q!M*2@DpU4bs6X9jSLnBe95N6aYlg;8{WL&N?eI zy9R{$!OW^Y7@=;~wMnz8(7k)N+ENBjIPTa`vgv7HC_?(V^C-LeZa%XL zK><$RXQ!0JW11=N;(b;hkwN!7bT0iFR008Z2vo2rW~l`9+o)hh2Kr*gtZf<>NoiGN z4Wd9F1Z?w*~1;a^Tm1BlZdeQ3r@Z z2=X%^9nKBjK|oCadLD4j z6;I;~=irco!;}7NeH5Z~S%+?DY?clv#d(qGgE~8?+})MIk!k1lc)3L-CkcX*9OUKq z;o-9a(_c!(-krqCqciup!CMBEbX^i*8_3EOOH%_nIR4IoiU51CaE=@+6K07ab!HY8 z#aC;d8F87HecC8%1p?FAU*GcK^glpexsGLRo`K%oVxKfWi(j+;6m-kr<@ay`%j~?A zGb#QRRg{y&-tY=rt9<3~TvL1B`y)uYZ*+sX^WQheb4WG|kZpkax7O^U3Em`-8#{m# zj4OBnGr=(M{|`-0PWt9S{J?a|U5^A(;cm7%*kIjxx2Fb)=Ah^dSiIfPDBic>9@v`3)j4gZmZG zP$AKVUqGi^zh7v91e#^Qts3X&EewQf>N7H-EHi{(FRJQ<6@&>zL+gau57!^7kCD6X zpGbfeGmgG<1hw*+j82vhN-;Er*F_wcm8z1R3cF_t+OKrHE~S4uBz(8KIAbK{E30Jp zEAMW!wjfgDRS~R8VI?gMPD$~_M>8hOapm7Oujb*4tX^vfMik=J{LLy3FFZQvE?2(% zsBzUNsfiS(xX#(jsYp0Th8~$9LwWG`Z@_Gb1Ma}ehs0p2OEbu8q z7zV)4^M=)Y1QR^^AjgLcz6V=i(`UEVkqLo~<4HEEhMaYHU$PtA16~HCdrO&K2vAmu zF(p}lEy|5<>V^?z?=^q!OC}-|E;TKu*J%%8GGJWMa+7amwjVtHwd$X7Zq8wSXkWs@ zE2G*aLUdlLC0%Y&^l)K3Sl~=M+*Lc6E1E5;KC>soi11A;E(uMA(CQ?KGMTuU>1jS1 zWg6FvfOP>)Uk#sZWeTC3ejf)52HL&}GDG1PW z!hsd?DZK$^Egp~uOV7`Y6=o?hQRv!EEwre5}ZuECuh!-%xUTQ!qd?Ow0KzkDaR&q;aehs zuUC4Kk_z+VqPn_!B~5gLpH8vgl-LK{Uwj9q& z)j` z%E7BG3{*EEMTQy0+K>@IV8cSx5Us9O!PhK@$Aj((j%*VpZ{I6Q#xj?HmKfGAhkAKZ z2bQ+?|Va}qo2<4bC$gp{Lx-i{^lOXg5B3rvaI2COHb^gsY*i|(?N=n5IEo5i+lK%N5Yit}&~$i_@Am@fPa zjE?62j^@E)KMjaKt{@rU^7qFLq~9C}jGqf>PXKZYoEWYks{P|sb|X?9QZfd~rzKIz zv-1Rk(Fqtz)?E0UZ_kX4zV6hNe_#LZNW$``Vw-ERxLmtH8-C)8s+10+RGdS86<_a% z;MfP$v!k~)&d(!nZ=afx-CBtDI;gl}stBX$F)yS;YSeNkAE&UW)FVm_P{lQU>Gvb9 z>h@*tnX*F;*$#M+c$sy?%wL?cijSQ?s+gdH(3(gh`kM%fGdGdMa=J)*znEkP_?-OU z!iw^4&q_a`V2zgsJ-2RfJCp*p&R&w|GdP5&zR%!`!KLrLoBFr`C;ZCSx(~qdLeK!V z0c;3?i68ukRQ2>m0jf8vtjVZx} z2qlW7DM}Cnd^y{{c_-sEC9>wl9i3X+zq*TN6|BQdoJ~ z7yA2A{4y4pFOW6H&!nGkMFb@0A8XFvIkz@P?zai`th&Ay#?$`8swM}#!yvxuCBHNZ zhzNv(-s+y!H{`(lF30y}f|5j2vkl;VA@XxdN>1^cfGtW0`U)}{x$%v!KaQ_EfnmV> z_r@z-{wy!+%fCY$NOqG&1uU{g%f`y&{cFWa56#RSDauT9G4FnQV?_lEt@|iQ zqt9N7Hi9j4xrj6(emK<|*|V>UcqmyEs)Uh8!Q@oAXLT5H1-Ej6VO>`uvUhV5((m%+ zG$hgHp&KrP-Lk1)Yf@F>Q7oqQ5xwSL2J0ZWcJBi(bkkp=TvX=*Z%pVdJeW(ZJ9m#) z!MI-CZeVv>CC)Ul8#Pi9!;~q(`4g(Z=481p6+{_F-cp0mhD%bi9*@&URDuQjg7S_q z@QDG4D$L0C>f=Gvx5wU#JDS)tj7Vs(&H)Qva4_cXL(u(~fVUI~`84tT!GuGrl~g1h?mM`fL*mqQFbD+r7oiGl?Vj9jV<9LPt-L zf@xzD9sBG6T_V~&@mjWmEYc>pE-Z>XU)~FoAXrI$t95Fh{x8qFqRO-Vq&kgIAR0P> zF~EPSAP*NPG19Y-)f|}$7L6XtgZ`tSk|OvmGtZ%Ed$lW&3D3Yw^vj(rVS08HCu}>c zc6)7D*uZuUM`=dfHF1wziUzromVo4LhROANLgTeD*^}R^Ls!Gw)_+gP81O|GIom(M zW@sDq=YE#X5NZn>D8-I?dnsFHuP|C#&YCwsF__^#!P#tIWBTXoA)y1GlB)6R+>|R+ zBsPD2U_d@Gy&iM@6|jK@?g&UIG-y#$hW=#ZgL*uc14!@f*ZO+AU6+gkBF^VASeeqz z==*=opnLpX21eS3r8ZYMuKD>!LjI`K1GD2zW^80s@HFHuaXA2T9pGZ*U7NQ7-r^G2 z^*t~@7toH-ec6dzy(geZm8%PQ@dwVE9M=g*R4(7)1_kl5;eg;^PiB^k5AIsMW&c$& zP3l=0;8Mm*RM?VG?ph-iYjl_ns#aD`FY~iS$vJ=GBcp+ncC|`nw%1{cyLKx~jqm4O zixO(i_-GQ?shto4<0o!?qJrX}ha=}q*VtTOu`e#Lska#xTnzV6(1F>mH$^}2vFR#5 zr01iL%%~KCRDJ_VNo&Z}Im(=Mi5;I)PUPJ??DRCi7-zGo9IW^A6x>&+u`m+nAm@I3 zX}H;&cGkc7@^^ntPt)MXo7mJ}tx%M$HHDP&a~684cEgG0I!Y!qh8Gq&W4z7D9orpa zJ4BPizj7Iqc{08Id*(%%;aS$<)bKc-qOjDOGaUK`_YI%kv79l*=nvnJeP}M zVA9E@(CNACU2JeptscNuRsFeX@eNpKSzmESd4FZ_>QFBaI zvySm_@oA_K1)hJI_UWh3yK>l_`UGiZZ8vgq&w@%4kfptw1&svTa?_czuGgu8zIz;b zo6Y5nEA#X2qRoP{?B0SiC2V1<-#r(lFhbEN(GmO~w@GFJrxgIaAkDmBPrwCLYQN&x z%IL6i4MR|yazQkyPXdHXAW{NAs1qPz0+PEZAWyXE$AOqrQHlwO5fI`F^_ZDwpn87W z{gp_zz>+gr!C@M6j5rd>W)Ly0k+&I|-pqcg_xoS&O)cP3J+D07u=A0&6>v;!R-n)R zv!2Km5a0gIt4*;T!@bBRWMJ}D-+as?Q%GE1=6$hFF><>~X$99{ z;xmUCPO~v8YD0Y+f@8ac`fsxAC=MP@rRvT|N%1o6#qSXYaB;KKQ+)>-uRSZp__7PU zE|5k)?VtB8V5GO7TCMvFM3$XE`+B=!n1zVdH>H=?CnI= zV)Z~B(a}t->XQD{@3|n;m9IKYh=a{K?UY2ZuAwv$^^6PiT$?;>?+u$0Gq$HzvSTX| zg7?xq2nUVFmz!E0a+G`iO*!PcG;J62&vfc*leNjE#Koz{4iL1P zTwvtjKQJ1_9a9gP9nt2DpG_O(Y;-+8>HE32TF1u3=(~hz%iT2}zeYwyy~GJP$1!Wr zSp(3Tixr>@pYSz)>28=*&uPrO7OWHv)JR&({UAVgV8D)}NuM~%lS6BbC>dmx7aui< zdu&%0ENMJJC>JE=#ixOofR4F|7b6X`i*SLWcS_>Kl(hcSUzvKuV~4|a=4fz z&D%QUTPubBvt1-h5hu?Ck#x-%q`a-W{r9f>a6%&VsV2Sc|9IlY?MZIU6`HyvHu(j9 zZc`3>qrBV+l9=jLe;&;9kIO9?1#6?ob7vvveYjzFmXAKRiGidr9(7ir+5CUzxk9&^K7UMM^!6XTW*c`2jyPI|Za)1<>gL0@X z3X(f`@K_mt`jqwm%Kh0{FRJUsjw}FH#O}wpYB8r@M-RO)07MA#k=Qu5$M3LLV5zwHNd-9ixK`TM)(j!Za!AUJ z<>i9Ks8Lubxu2l~g3t%VR4k+WnRI6K6a@4SgoDk!7cES^c-B2?pLo`Z(#A~Vpvme^ zGat;p#Sp`~C$(ZMSLnuN{L{YGH8$2qQx1`nVUy1BK_+{rmKbrrpN2tC78=r8D^XKS zepb~-n4PcNl?4094$T~w-DO3x7+drvJ_AFXPLeTK3i_1s+tu00EiuF#3lXfq{@#e2 zA&U&k`WvXqo6FV#gfGZ7`LT!P5sHA@pcjLmxa38FgmQ^8$0 z@b)TVp+zyYguSZz$SX-ZMrB zcN^pc9#d0uL4P&9 zE+Q@4cJHga7XK|K+v-NBp`7E~T!I3UMBD1Nu=B)s&p#cXk@Fr`U;5MsQIiy?+wS(V zSz4u7_?$2B_t!xVkafUm-S5U3Owsc@kvPoy5ex0uQ|mx1g@l?MzsEY-*c>rkDh7;K z)~Eqj42Yyd?2o{uvi|U6o#KmG#LAX+7^pvx9fqpE$y1EOA+a61(DB8)&YIlLTW!)2zG)oiURVdF#bj zJRO2J+PC_VU*D}x1PBnyHYAK7tXp2&&Qmf`OyiywHN~U&Dr`t|XrPZqQj{P*>?h_n zHZrCVv2Vq`r_8awCSDsI!CD0Qc3fiO&aOD;W)V4|ynR5_DQs!EiDC=2tRaQx@w>kw zK*5$PL5Bz=s4pud?8Gsbeb1JWlGA=4#pf8WNJCs)l4=p~=2JXnSz z>me*7&BSAy0VntSjlS(Md(}p!)19;E2OTy?vNZG*dGq=u#%6n0;7hSvsXlx`ML}VI z{~o~m0>1DTf${{wPy^Dp6KJ~v)W8A27PJQfrO09q zbLaKgtafv_YOSqH)ZaY1h6cy1ePxA_%BRHFAHi)A?}GaqaE7!Z^Y9xc{Ah>i2ceSv@tnOuuRq$vw_TdrtDAqIv#-VE}iHVNU$ z+KI_k?jjbyKX3B9^ITgvx`;ZBPXAFn-7J~%kVSu5c$IuT9VES>l~4zrMkM|~EcGzf z_kF}gW{1y;Hpc1rpY5-e8^rPPah&lfkj$2)rlEl>Wr64a@u=qch#;5SpW_&qetdh6 z+`*SnAE5FG`95XPMES_*M0wyak|jb36h#iJVE7H}n6;{v{?sro-{JUUJ>aMK!*n(& zjYm!8uMJce!pEt%uLW2|(CX)!P_=^<2z@DN)5TZ8<7^@#6*#jW{K;4>_Vqe8*YLVX z=vR-|(0Gw5PzIbbVp5NV)r>8T{Ywd>s+Kepip-?7DWA~_6sv6U_)aj>>d%8!K3ZD) z1-GkZstS)@i$(sBu6n%5?f(x^(J))^gY0ed_&ru^N_23*^CFHmTPkVtaFUYcfxbYz zuaL6Am(d&;=NLBH5^eAt1476CkH4LnS}4Nu(#q)642>~iEnSJ z#TIA%ep|Tu^gBq*?#dkE(&w5K949sRzEjbnLxPnpA7wR_;#BX?8Sb*YZp0HOSA8%X zb0JM>dgvRozw%?{_`&f|hkwX3HZ_?X?^h&v#Zb3su;mxa_BfpPhyV_eZAe#uZSg21 zf)816x9Kzj3&E}}teP8AFV)vlK3b5ev_G2f!%o^ET6 z(lW5y)R|`3M6h$P*T!X&>#R#kn@ho01`{q zoB88!K%;=5Fig!ca%{MMXEe&{yl102ZCYQ+K zOgD6?BLeCTv^JZpCDBgwhQ;O>Ev`JNUH6tRoum@2F+3dN+)%p_$L$vY2 z*R*&mC7gm%FPU_W3y=md;PV0vFk{a}aG9Pfntf)8aUszn3C&EEx{D(_azmFwH0PCn zEkCzrXtJlP)~gfTeJi0ZOxw5>?OKvc#O98qI4y^wpFHi3;1>OWlfl5&>=C($CUyql z#(=HST+UtPJbl2^V!${gqzU=;m2|*=Q6+$rkq35QrpO<_*R-A_cQ}caFOLURfuk|R zBg?p(AKQI7(%K`25cytIn-~u2bg^-6a&#)PzR~KY+JL_)nI1~Zv?r0{o%xDYqn7zs z)(Hu3^i!>jEP=@G4H?PF-=2vh69}T~IY#KH4R>gBO-58_BQlv1OG+sP7&c_l30^pp z&4r)oiRQ`Iz1 z1;pYAwy?iX8~$;r(!)7x!{71o;ipnQzX~EXU1=RDB)nC45b~iZ-@v05K-2tefUF6Pyut564g2;@m3AvxL?J8# z=bt)yKK7NMoM0~NXbk7PqGYHRCDnN+MSY|Fhh*yXV1 zrBCdkT}8OiGOuIVvRmB_tsBn5qrU#w{j?C7Fvn9g>cmUcE*_3Dukjmy)>_DGm1R2Z z#BBQlOCc`S5Es`JZN%a*YW^`^_#qE7dRB%^meRpb!1~0Gd#86tmx}p34ce?FuQ7kIHW(L_e_2 zl!YPmUmXl`&*Bq}(f=j$Mc~mv0C;$~xUV+AevXIbqo2Ebn>ORj)RYK&CS{_`Twgv2 zWIMkuxB_azpJ*@PO2@@IrIcM*g7Oc9?LtU>q~iw@R_KXtE1bd&2b z4Jr9oFkOe@2=mo)^Q--dM?89yS6De}K?NQ8*xH+)QS~`lUn&&?iR%2xD&fudfWGS=D{T*cBo^RB4=D{zZyfKKVE{`=P@NZjJ!;wYbXbRFTk3lr&;7Q^l%J$8qRQOLoQua zLp+x{yQAel9aH|u~-{m`|$JEp~t@|+>1_uxS1D9JhII#Co) zUS>-z2)Q`{+AuTuA&SwkGb)$h6Z3|+{+}!kOB9INAEG!N&XlB7QjOewR)}d%8fqTv z*$=d_&YCTY+pzA?LtzW7&rU=s7JlOQ(eERf zwN4tQgrJVgl)h{MOO>1=$wxD@kkab+3XUAvWpPt$AEd>_Dda>dQ!MQAyWnaL$<1{z zKhvinRhi+fP*>QcXmIhQH7v)G%X!#rFiuP+%rge&et9?k>aI=wP7|MGU+9Z2mS*;- zz~4|2qt_)o6jUG&0BP?5(WFl1yV;wkax0RN*7hnFqQgoKupMKO?2fDfkG5zul(MnLaHOlfy4;(2h>&{{ z5K1R}HLOH0t3-9c&=FyYw2W(l9(-URzpR2nT1{#xkLcgEG(x7knrtUB^{bWkH|}IK znYbw)h8J{a=wGecl9&h|9EZR(44ReB9M7L+L0dzr)C1CVT8O~FnnB>$k*HPyLJKdM z=*(b42Q2K6+7nErgleKS%=_9vG=@7^mdiCgg_CzC^3@fWj3f$sAy zPKs1|O?w{oa~^+}lt_uRU>Z72vYMc_%QJc?%^CRhkCGpnM9rM5%)PX~RR*!&ekd0z z3C+(*v&vJ!)(*mkqU4DjIu?pRDnw$faIVaflVcO6yIb8gznj-DPSt)TBHE1Vwg)uRH9<3?&z6NM`bOaREOyvrznuwkoLmaW( z!B7{%oBG#JaxvfV@N5{s^V(UkGvsan?k3Z^;68Zg`^@z@uqBzfXXU*(jY+Ed=S#dE zg04HUHv?8urD43RYmLh8%=E%_yc${lSiS+H zGOt=Q11ld~W0|qMIcd#6Y6<7o9<`SdZ7S`jO!iJpTF?_q(uKN`6xwQCTpF)E0f&W` zZq(SSLqa7uY=AmHGyPgvvCWbV5~Ky`0bu7aXrA9m)fYu`0_IhrhowoEbq?Vj{_nZ@ z9Ombn-lLhxnc^ip!3#o)k=VbzT4gOl3wswqT^UQP()mewDSnk@JM_Gb*#fC3iEyaR zu>@?vo?$+8b6MS+SVSaE>4GU4WU0V7*)8VA@3riv7|BUx7n=B~8&1P$>Kk}xelmmI zN(dL~uv|nIdjtgb{`Ssq6o)yR-ENbsTog0~!{jvg=C~whkvVLF3gq$|O$(}Wcgc&* zdc&%wgW|7EWkyAHoJ|Z7z9*Fh93VwwSNq7{+YX!6zeiv*73NQ5NpOmrU09AO^E=8~ zwrOqyHeIT`$yv`w9=Q7!H70B;d+XAoAD@4GHzGA^I{!@J#x+2P$u!1dg~PSRNYc+XE++B!x>mm@ zwC9+o*GtZanPy44a#r06CsJXlqGR&Vj~%*O+TeCk!EV}F+WpS06$ZOZj66?;ZQaNP=l3#24 z5@eL>n!aL*nN}Sm2dw07rW?y-LU8ZOSLg-qIIt;G$ty6`F$SHP;ArLvrHaK&BveKh z#CewzG%MW$Im6^niI~O}HMU@P70FWi?OWC+)b$_;@opNSa|E!hU1NYf7{CQvtVoGM zi%D-lE)bYB{rEAT`n(OWt>sG86xrR*xB7GN_(%QxCeQ!u@l_w{i@I(@J#O)+@y;!J zV#v3Ueme`!>nVI0qszt1*$7q5+e{LDQ*WX&Kefl10Ckj=V2E2v_r_sZ`kp$)HWj@> zJmJRf3?r#JXD180f6FZ=w1QGkn)*Pqme#nAeX5b4O$n>2gY{^;os7d@|MN;^Yf1lJ za&)_u45jl|Xu19fy6~=;TKbi^Mw2EssgM|fKSYc)p^b#a^{i4wZ%Bew@{HgNZv{?` zWur+q%08&-0Iz+jTxet@0&&2-H!RJB)9F3IM3vXBOWWm%c62{ETzp=!?J%EO1tz#< z9UfX|CClnB_TM~lKR$!CFThr00y`@N-2+6rwJGhHRBapzhq$=7q{;omKYyxMxw*I` z-)9TPe{BKEeb&lvy}HuU(qA3k9DrbC?&6XJT6Gp)K@iw>E6vEG^~_UMa2d>)UV9tw zsZRnW<#AXOH5p}f1>EwHtcSP0=9f+E-MS^J$-4Gro9!>rTgr!(hENir4sV)L^$tgo zVSS!8?~SZLgu5WF7E~HRr%c(=J!K;i4{#2CHVNX`B@ZRt&{!J8u4OP95ZjO`Z=gCo zXWOGvH#@;iV^!6X-yW*vq6^LMSKPKnU18ZOXT+)rd>5tN@#g}iu$8@-hk_D>tGGnl z^qCj$*K_yDLN?U%?lFP*u;aq(@Z;ZyHH72SVVCfsFwT>eF|!WxBjL%bud&mloFy)i z>KJ9b?7R^RVRbgC&n~Ik=6No!HdL>TTahr#%RhY#R>{^LYaW{+W2?rM8Chtv<|XTL ze?@>Ur!t+De(7z8HKE;1Nl>WTj@O%>*=InOqRHXgmf~B0;yWNUN&ZMK+9W3K`bLLe zYCnhinn-BEQuv1dtHJlI=jUf`0X0ZzIzW<49>d|A4((qGoOQkV?J~*)Eh+*!crq2j zAUl=cck(0~KcCYl!_Awt!v;xSv_QKAg!=-7SG(2rR0;atzBAm>?~Bt5&E(MU3x*mQ z`(?TE8N(m1aKGO?MOcO@IXShDO(z-mac79ZZ}o7OlM-?y#)7bD9B5a?je+BB+g2Z9^8SV0hkjxX1a zcYqqH=eqd)df7IU+mRR&(gt{p74N|6roA;9;=PZ~=+cOfl61Q2Nv5+x;@F(j;;+A! z!H#|%g0?HK+5go#-h7To&3fhY+C}aor^cHEYz58U#cSWs_Aq@|oHY4dD7zmsc7DNQ zVZ=r2Le-~97Jb7z=jS>)a#f9T(=A+1QA{M z*L-KJe6NU9m=xI!vo3-nX`=5GiaVw8DldmP>P_3Cm-i`ZzJx7jK5M|q?2 zuRdYXA~ zRoxyX{51O_N}u{Q=y9om!uNJ!h1aJ(et4QJ$1ANhV-M(>GWN4;poav~So2~h9C^>f zk}DWAsm^G1M3y#`Xp<8?{rYYi!*2Da283vfHCjbmVIPMR`rc!ld9g)CzA}(|yK5VR ze%nl%fG(VrVQZ7}n~!^EJ$h*QDJp4nBuw zuw%YezZx5NdNCEYcyd41S@kAbG4zTIU9}CMraD-{M@Vk~QUQ470{cZBfR22qG4#_3 zX)$9T1}|Qj5;eQ!#z+XM6)gBW$adTE<^kTe$2q6P?!?Li4I+?z@f1i^0}Ak2g|gpK z@OfA7n?bM20EXjj<{Co2{*nH-@6d_yhGxlOelBDdP*$EyYI)n$j;s^o^=Ke}kTku6 zEkKqE(|!9Zxa%FJ_%L@4E)QZ7%Fdf9bD4<1JA`3Eny`1UlU84| zwf{qoF)0!8Qt9nRHnF1LyX5U1dC@X(@M^GLWu%l%U70aa)Rx40>2dTk>eTA@oSW6F zXrvGEGm9<2n3Vp*c(GEhB-noCH*mQ^oGb-&1lxUUG#MNLT-$HL%QZ?fujN{$5|k`@ zJ8KtGMzbD=NS6jagrEWL%qb;6TzXT5Yw2v`y=;5g&YK z#Aw~)#vKO5SzEScN?7^2ZmKlo)F}K~JWGvF_ekpmet zq=$R`cEG5?>Rb&hF?*1??b^RCYJ(zJ;oEN1+f<`FEC|m!*ZcKL1Q&)A=b!> zXJqI!NO`g_h`$Vv>%^czkR8B2ba~maR$AQp>Zh6r0`3=l3tyX@pGytNMYEc60~>g> z-eHl5DNsQq5(c)&&>tU%UY5COk=3$go;fyj+9e7S2RN~*FWJzMCAeZUP zjOSS+552LhfFIA&u(!i0Ma~E3>K_a}W>q5lm;jSyn(k_U)jM4{1uVqk8TUT)%&ZQ3 z0|%6sbKyr=Wq~`^*?#CEnzz5t*02Z#TEku1`nLLOSSs2;W&~2E3LuH^o+%$Z{^pa- z4j&Bn7A(c3lI_n?gh)JKjbP9W=dFeljk03NZ&D|L{+!UpOM~7R~K1wVb zkALtZa#guiyyooN^B?5OIeY^ZhwOv$n(gS=B)NatKg{INe(>J>+O2S$@MwV~1y3JG zj>}Mp&vC^`zrfYzX2#UsAj{sY*>E*S{g_h{QEX;;;#`EcTxSznS1B#eIMJgaSZ!`9 z4=2Bz=vE^rKe8AJ9jft-i`-1xFg-1LxAm72q4kpSiu8lVD9oc=V!rOj@r!=wRBPX# zeUaztC%$)Ri`4-){L3NWj#U3?+$ML~KjFvc&j4$EgA5ldGXCS#*60DBes-@4@#-`xS4b>FKd`yZLC~n@eL;y6H7piU@E0Z+6w(PLmocSe7npx+Lv0DsK@>2~ z2yvX0#l;-NMd&HA+ELU1)9Ny;RZ4p=WX-QMqEs&tieBT|@U4#tCkO>5`;k3YSh0yu zv58!UpRTy^tV$i)g_<@_l#%dULS`zf>!$Uq3U@t!Cpy&)xc*(-JyH$lz7R~U2Wwtb z3e4OPs^?C2)~Q}AdJ&GM?@>v`o96tyE-#5gTMp)y?j(HOCM!a{q#9hxSs>j2MhTE_uSj8uZBnJhK5-XuMZ zpOczWLsn9H-`8$Xasu55O>O2EdmaI|Jvp}}+xqi&$IIWBEN9O*ehDvk;{o|i?i0sy ze+&Go?n3-HHn%jUm6qrgpt@6DNWiI~RM8m3K@CR@tn6Y)aDl{kQck zz4d$S^LtKLS9jk(y?G{=$?=yvb@nIjgv%z(Y$e*-IF^P3yY#{vPMt$_iS&Nk#4PDW zA`DqH(ulfD8R@lFCk~e0;!%?SWZC0lnTjSiH8tZiw;9Rj8Mxk#ZO<4h8pPQ#rr4y; zRC>Rfb9*WAQ(6%=slgD$z8(QDKD(CdHtSOijnUhA^yQk5P^d&2=RcsNX~JYJd~`Y+ zpZ0rH?Rvpm@X>Ly;1tv7A=~jX^yT>F>p#Esp%F9o96_IEixE&JujuK?edn87*%J?|wshkB>EpN!=A%*vVANg!q9sBg*DhlQR7PX$fT&0WQ6`U3wevl7-WLa zY=*xOxi-_)*g(|+Z(KqIlvzQ-NHDD^t04jl{8^K-Pyk{}=f8UPV+PTvSvBAkxOV+} ze{&6e6*U>K>Ov_0)tm>; zLIDruyIyvLles?k`LtAQGutLIU$;~@r;8<4ojKRMm=im#FOe_@-}vJ0Z-d7_dj%Z1 z6M?Bg|CX)}6F6mod+2lRoSU0xDAdji_@`M-wGm^Jo-?_E5r*X{#722hdS>r{FOtk; zGQ3IuN7GpbRn@+4Te=bH1_|lz?(S}oPU%kR2kGwa?v{>Cr!-1;cf-5(Z{Gh0zc|9+ z%x10ozOM5;j{P*aQ1K;5rn6@ zXe_exU{twMQaaL}I9@x^Um>Vn7Wy@bmHL|s8s5vxF4jP;>hd9kJ<=qp`UI+l#qpyz zC~&dzz(h>?TcYpdy~j=X$18HMW>~7n+4gZX0TYt8Wl=C_c(_cn5`dIoDDn3Jbulmd z7u0U=P0Vt;D6E&3n#f2=v^32~7K)GDXA3B%x}8|=;fVamU1(g3 zIJm2uMwqZd4mA2Y0}+T&1`R>EVLJ9%$5Sl_^e=)YMk$qt`-|i4AbhN^&4Br>QH1Qd z5KUgV?gLP`W^mH?0YP@KgaLABAm<3w2M!=cf%Y&kumH+nBOdEB8ym4F9RMd>rCgmG zaCQMnNMRZY!CWa@1pp&cSwX*|b&33cS%9EFu4p#!CusjQqvwBz-y(F*EBF_M5P+?6 zFW^!sk!CzDok~iNfj7vLz%MCD$|zPCqm3Mb`G~G-(C5|;O`w}`G_}CeT9a-@hGX$# zIE2byfy7_ZlVW0|S{r@boe5!o*sAFw)ZA;2Gz3T~+%&I@iZZu0hAa8K4)9;jJUkg~ zJlb|&zI>$n2;R!^*FSkB>H76K`QByll?LY6LD@IN%UaV$wFJ+wOVT27%}f8yYA=Gy zJ-8Q))F7hDr&o;0FTp)>#ZQFSC2FlagVa_ zWgvHg> z8k`hhAMgd;O)z0!3IN0cFs~2L6=wLv5bkw`hdn*dGbxvAwmZ!eT$Gm1TZ2oDz>!g& zHn|^n0B~SB5{6SE?K>S@qVXY#{XQ`0y1lKTN+|<5Z^P$n!^%eyepPXpon7b?dL0nD z4E*f-(~Q+?sPPUsvoL#{ueV0Eap^6WC{thlQB%6g|Lpf4phNxNp$2Gv%pMSZJy`a2 z-qTIM3-?|c(r^kmSG|4EL8udwpqSR|nV_lJ*h(jNT>GG3?nsW>Il&+L)I zjQ{=_NeL6B9aoCiP1Smb`j|>za%9cVr}|YFuYK2e*{#G18>0kndxQ+?+y;wD z0@Pm2s)6w|!MDf-o?f|IQSoz1sE6R@=ZAjqQzqmsfQQQgvP1x&VH$Y2G6>$+$JPU= zOt1w8%;S-gf&&X$a{+Z2Sj!Fe5CBv{aN)WLSeXL%1t7_#-QzC+Oq$ebvFtmJN|458 zXlD+1b`Sp}WP$3|4f`oi0YytGEf?1!y6F8KxmY;PEnfhFg_5dvHe<8OxnI8^$WC16^C<~lWd-9QXAzc^ zc4E*S(~OP0?q^vK^89%3h4u`|lxEx~i&t4;YO+<2r;cja-g+!NlguNIDU&hJw}UGt zEHXC=*+2_wM}2iEp;s*shhGgelGH-dj?jb+4_m%e%V_tfV*|!tk06du4fK&P2HEVf z(eWf#X}5mBIn4Y?0JL)z83PaW-y5xF$$;YWQ$Y7qsy>?of&qiTpanfEd2&4)RcoW& ztgLsyfyX0YeRUbzV+DW1D?bvzKhd;Xb>L-HedU)6bl$>&WzO|Gz}9P6MRaf#00wg$ zR~h<@paa1Np6v+kr%@m(;uU2l#>aY4*H9A*<3FJi zuHs%5FB%s~IWmNv|F0v>TEp6vdO@?(-a>d~!t7rf5;_gr&2*Z}bi`>4%GcPTB?t|o zO#%f8IC=HTD9ws2`ji!3UGpONLaZ|A@3i=pCeoVhtN?^|93v$?-nSba&%0PPU!Spu z9NNU+=I6hamC@Rd=US-zz&i_9dM;(ss%xYb#d@h8hX1V9|sIERj!Gp9$qu zFQkXZU|>B6w_7ST9nCJ;uW}T(WjARDwmT?vs6_tV%yRqMSf{kRRmYG1N?TB4jW3rq zvnSPUaR%sOm&f4O-}Yv#DLpV zT_qaDXh9Aik6tyMPn^#7kZ9E7-^vtC+p*KbuzwY^0>p|fAV*m=Z%s%wI;$r3pKQ!i z^)CV;ceWk!9renL+1bY54Lg*EUguK2kK`o#QCvf-8h_bkGEKu_ai`@QY@Yh1Ti>(z z62EN!jy8j>A|L>qMPXgOqNQEBG2f8`S)gZOS@)jgVTf-qtiM%I`Vt{R6J<=z=SZ^% z&4(yt$#*b7yNn}@AEyMHgKSnm)sALGbBcbYOi&h=9Cb{b?+&j?K|{~+_C})9wDSZx zKmz-Gz^zqTvcVLx2`t!+?VN)?%?5JqtXTI^0gc zDL$~uSg++&;UfU(bqxRnHDG(CILUht{5>G63rw{ErSBCS*cCt>Oup zh%8k)f`>h^i9*sFf$)XGbXGPSM0K1r1}`GsmvkAtz?j=djtte!uc5uaD!!YvDBMQ5 zOR0bR%;qpfS3Di&p>%TCF|Y0>%mbfx&99g`PRNvkTMhdNTd~Tl%SxCNa%D2&gdW(( zXl*k4U&R-dA$5`O2LI&SzmQ?(Jj*~Bz#IN+$0)`G?5%Gfu(y#CcVJ*Uz^Q}+Rvpgm zQ9B{UJHT=yEIeEXfE0j(DA~2;oj(pRV?C>Rkkv z^nDl0$*3mlYTM~6{?8kq{{vwh+_eRfwrxHXCmoXhKGdDJt~tsX$Pvh| zHfq=Zh8>x$RiG|=Re8~se0)n$;;;Az9YN#r_&9^x{S_L_6$KWzz{ea&x*AC_MPI#B z+OE_Ap&Ik}$3L}(vA@izQ%k)bw*m^3t1Ms63$s?7;`EiE^gfg9ye-vG#>W8LXf;DD zJz4#6GI;hF^Q{+%Zf4I}r#Mk&Rvrm{0x8>QW%P~DGmw&F=1FK-HR`xDUQs8X3ZZoM zQxoSU#&p@vVK99sk2~lY%6WZFBILunl@^J?}iygQCl)cvAPqN(U zo0)-+!GvN1b~C=2Zy6?Od%~7f&WF7Rw|&{K#{wQVOtFGffGoZpJVEe(ea!UU03;k> zrsX9-v`*T3Wsc+>__PmTe`taHsl!;@-mDZ!F~De>2Ihd^CmjFtVc}~Z$ty(733_b! zflIk+e!wQPu2#JqIXf@v%{)!Yl&#nv-5{FPnZ&_@9hD%wZ*&9Z>XLR!|7XY|G~-b{ z+ak=UWTdQ`BwT9w`4o9(t{gJsq5cEB;G`hZ8F(6MZ!D7(4K|Uudk2O#+dX@N(UWX_ z2)&DQ{l`rw8@%{i{cs30nRZ65{4+A~XI7a7S%wOpTvoas&`vaK^Bg`{_NOz)2VG&T zdDz)oU0(luBB4yK6cwcFy3%4vQLkr^1ctUO(#)^zO~{C?a<%4e1f z5c(fFk;l&)0H>rjx}CVt>;9R{>DKaGYF=XTb;pg$2Rar&t{0q~zg~CI)9&&{0p17S zmS%pkgk9CM02$xjh#kv#TIf;$AqOmwodOa1ju$Gu4#S1ah8iIDne-=iADI_q)00oQ zt{-<|!K`WEGzH{IEhms?)XoYvExCVxP)G8z{%&iGZLZXGKAm9yE0;oUmebwkS7p={ z7!$MSX$eOYi}m&r`-mIiO@HqTFb%I&I%snWbJUVIZ|J^ z?)YX8l>JrQFS-FVDC&HtFVBK(sBX2Sk`hyGw|CS@&Ds##ORmL1j4ijvIsKD@L81ABasqrT9z% zOk2WAO*0;^N6dFP|3u5&fWRMX?m)d#nlqI=cbo-q)dPHPZf}2lD(<0uh0Mdl)7|xU z7l+llQr+t7L(|x+!eFM$4gYKC8YmaZxoUJ^SreH~bkKf~d(aIOlT12KoKFTLFkZ)M zUlDPb42yuTDkOP>-+18hxvz@mbw_z2@%!mk$;b0LrsDma;&l5e5q04F{K{-H0uU18 zU1EiX!yVQq`%;zatlwxQL8T+mg6&b$p?xZx)kdRqps6~qH(JG9lA`M72nreMl!{x| zR;Sm68~sKbT|QkISjj`9rEo(0(&El!=jNyj-Zm77 z;$vh(2e_Tmu#x+RFP~%P*47k12N5PLnolBUlbB4FXz*VL&pVu($havw!0o*^E{=@A z1N#90%(&PrEt3H5@D?QjcgX#QaGkGJP}#tl3}Ci{w=6AmxY)<{(qZn-d1`8yB@AHsA42%?n zKIT7q@7D0I2ra)DbRLd;GknR2q-_7K$xV4;cx;@&tXf`QQ9hdSiyLB^n73cuD~d>~ zdKl|E!LT}uBr*Xt#?gsr;~R22#2f;%==LMe?>vB3&}MD#j=tnr6q)KE0|Ou)Fx`&} zRkd{Yobf+jWB;{+C)&u?RIAcinVzg+Lq)Iom6lzjVe(aqPQz5NSlOLIp+6 z0x@)r+k=ZApKWC*$NkY*ufCZ<4Qq_Cm!z|XSnv7y-EDJKUiA;Gxx2ZxRabomEO-XR zGoLr8${Mu`NxGz#9bPw((3WyKp2_XH+^G=f@VNljyXu^WF|PA_Ka6~LppxH?5C2Wi zJF%1z?;tR6e;=Lp0^;vsutt0LfA!G(xroM>63QQ@nG9%aSp3gne(!TG+Am8Ks{(h3 zM@WE7=rbU{4R~R^t|vYT@o)tZSj+8naGI{gq9;@h2z6SZ$WL6J09PxlTsfMd&CTzB zOTk>Og9rbM2Otr@Q0otHgBTTa_(vW5u6N`oqpo-JbL*FKu+lc=R4k(ng>6usJBGp3$1y^@&XEf~*)n1cv&F8|KbFX7n|O0~a*VFYV;W!x5#^jmsjr`xwJlPvbqWQP2HRm{rY6y()BU60P=}VMyq{x&p=Bcq&~0h! zMabh+(T+pJk!WSn!(l=3o75Rl5{(Cm9*3F@9``glzM>+iX-3<^nNrfuS*|ClFO-5m`1yo8mlrE{`sZ6Ei zU&dF+F(}d|#nDADoSz&?r4JngykP+<$DXPLAvR-DgRkWKoOueB3gH$6z&JR9$b+ zIT`;V)xg-;U+V4@5~e^m>ZSSDa8~nBTV;m$r#!~xBIb96MRPx~z?0{e*({*C;?Soc zu*?MW<0_+7t8J^fLFZ1lSkX&H;L-A5w^mU~9lS`K1Lcx3X)e>--2F}vX&wN&owuC2 zEcL0e{Ma(R<-|-&PxqR)cPLhk00e)m=vXTa)w#ai!8nFIB6=P@XK+SacTT^5mYh#U zp%S+Y%eAkcD7sA4$`<#}zvS+z*N-8UO2YT24SLL~Zfco^%vmjcO^bXyN>{Ph@62LG zMhMz$2&7RRqYOU-?qUFbRRtVZ0{;Ocp`(}VH&5w=M7QdFCW(XczP{;(W9P|HsnfZ< z9{PNw^8qqw;Gc;WP-sAKx>Ym*}SwRQK(BHo|! zqMCm<#Ieyj9>S&RwLBoDw#yuO^U)y4&vV8D}y zOa@uaNxlFjei1pGrKah-;1^p>bN=DN5HX1%JAF#JN_o&CaSPLDJm}=*m{yLqFlNpP zNi^ycvhW~hvodNi1^6WBE$tZ)y2M#Sn?fK z%Fkyxh@C(SE&WQhmJW3dd*`q{mHsaVXWfty>Npi+3gN4E=QHoKck9gDvuPL=j$OnOd(wS2h$hsQplRPUs3flj=6o8T4(*+((W|AeyL&>u%FB&vT5*57d)?9M83WwcPuRRZH72jO<7s7GI}e+Eb!?&X zZJ|!O`b)(2;0nt(i}1Fm)e~G25^ytlyA?nPrha<-o#t@0^;n#02sRx6ff)p?Nft+E zp{Fq7S6@E+lAk+Ib<$biqQ6C&T|~Fm`3@mQu^^|Qir6B>#~hQ=F%F1N6&83A8Ql!! zat+rt)Iv3juh^#~NpZ%b`_eE_5HP08v!r_6FR{YN?Ya&BSqbtou7pkCT0>1eI=#{} zP`#XV@J|G)KwI3X4*(o(`u4JR!3LPXERkygy1NL#?xAWpU_t~?sew&3&?RZ73+wr9 z3Gvok`e*3m?v$`NNsid17^G!71chUvESex;(iF|7F_zaP0vXTBFNmRrtbq7f#8P>G z%r5$!A5Hfw`3e5Px3Fv9`Vy1_qCk0Md$U3XDRyoeN-dMVV(M!(D14kIhw1DXq!r;! zuQ;bOlrPp+?l9Ti0~PwH=E(8&lALEEF*PlMhzbIaPbq(T2X)wA=(&Lx5DH3YD0?$k zCCQf{`M8}HB9iUxWv>{loP2zZ%e4K@*i%a#F(y9&KY7I%Hg@jbU&(KBqTEf3{MRGo zx3z}jiA=-?dK<5Bg8mQT)+fiCB?1=%H-3xQrJmat?{G!}KJg56RMwMxS(yYw6SU4T z7A=~4qU9r290j;Eumd5z;lCQ3wHP=!v}OpT*WWp52jva;6>Nu1SDqIEAm?* z3rs|ok}v<1%Kvf_HDQn7q!=cxhOMz5Ka~k2hu#zaLK&uwNA`nR6@zAA^AehX^Uasm z732gqF94~#krR{T9d0+LZC{}`S^7obkpA*-cHlP-SDbhsm!d?~Q zJD~Ih-TqUX3W5kM{1MaAyV@z&<+!sNxtcQ(jpjblUqnt6sd#ZCObnASn9SKWgt7T^ z2SFc4|FbhO{ms`4QPQ(UqcU1`)sj(^t0K7~v1`TnnslBP`VB=rTar-UK>d<0Af(g( z2my=EaX`bbHcdM8Z!)Vd;V~tghy)MU12`Y?3AK-tYZtFwjWHrfSWlTy*K)#sDk>C2sj?iq@fSiPXC#lk2?y-D zYBztjIuLHSZS>EHY6pi=wQ_+G=@2+ffQvxKp^ebv4ai<$ zQn5KE)avZ!TQT>Lbk{INna0yE_I?9l{GzfUxqHjAP`@3P{`#JsY_!{LZvqos&)fA+ zg$7*|HBFz+rJI^wLMoA2Wp;#N;ETV=&i6EgP6s@@xU#zg839Q`kdP0$;`YI z_-fapE@0!1Z2@zLKlafxcjT}R5)3#@490R6mxtC2OAbd4m=OgS+pQlKtG`g@`NRAT z)}%JYt8?{g z;Tya>G6x=wSX6_&|Al-N%`B^daHN245dGL3u(MC{Ewkl0(L66{EF_VV)fWOC9>TZV zozIQ!SSk+>TNTvxH->jl((T}!298*vLvd7(O3P@_-0!Yk{{Gpr?_QLd&p>~@zJjCE zOa?-l7L;hoj{rzejs9=Xa-5?Eo$f4vE4}wp=lv@FWAy9PyxI!KayWj)PU=O3jb z-tYWQK37GJz&n%iP~(m^srH-sn{8{Nns*jk%r6zAv1xwkxwe7)&DzVn2#b zir-JB3UX!W)4C?$Dkp;LJx|Kz<>=-OfyM4WImH`V1Sf&qu*LId=`E%gU`Gi|t21{l zk>Hja%#nb3S&dQYFs_A#h38YU*5ms;oaeVpF;{bId^$3LHQx3vuJ2-EISWtU5Je!E ztaf+P8|`d$@56$0qh{4BCHz@=M_fMpW-F~X#a>-q$qk1TB_T)QARrNw1(IsEMKC}X zHN+&!B3AHxmTET}gT&x|J!c@<3|AuDByasEN+EeoK}o=GSh4s+GEo3N~>U!SGKN zGNKA}CfS+;tNq?QGTdtAbDO;1^CoZbK9dg@rX|cxP%D@rH#Y?-mcXKgLJri%sK{rp zML}ROHH<7;hbs}-dU6P{@_jBV*DF=$40IMe{FOBHrypDQhV7u}tCp(RPmMItQSNM} zUm^o7#BUTuC(%`>(Wk+lrIxVn%TT6EK%*NsX^{?~JphCv>BG9q=YVUffFRXhN&(OF z7e~~a5A1nR;&`n!{93BoV{39gMmJb)vbDjJY^l~`8Sv0KbXLwV3OT}ULBE+sihmYz zOdN&`;#TG z{S1kRGd6a4xy&3J9T4d@NvD&7u-y43w(E@NJFP3o$Qh*Vx>p>ggtVR3rdgz!VWOk_ zUFRD}9dmwHnylC=C4ocXZ9ciot|%L z6tj6J0lkNIw=a+Aa#8`XNQc~9ZP~N4rdKIQ(O(NurHgsiz z!cj9eU^kforgN^@cb=i#xozD0Q2g>dDo#DFSoiSe;N)y@J6Yg=+ziGw1?zpKVu_$Y zTTI;_)G9Ool|th4kLs^5n?q0Ha&qEp9zb?9>SVkj%TkFa%VZ#-IHbjA;0cWfYUS?k zjY8)Sw{_{7T3H>U3*(4hR#cERg_Eq6nUyo7iFwy4Awlo`#NkYKoGS+8?_)R*&i1{T~fwgJjhH; z@aszw`Aq%aHZd4;HAy5#19(F0RUFuDWfm^&K7)Vi8>PzM$)HpacgHp!9E1Nl(eoEy_LiqZq z^>J)u;Oq#<4^EbW(06vwhlKWpf^aJ2{NC)LjIN)EnB+`GyqRN!vRRSXzbj_RO}-Sw zG4F))s_BL(C8PX^s?>o8{-8gr^U0{)I}1iDvgoA(1?ogXwXc|*v40a|6igRjP0?wa z@f5L3(_+voB1{@I&C0M@iEW3;(opjWMLBes6C3|9Kof`X!g%HtAyW!Tegch`THQbk zwRuLRrz4KvMB5#nQSEflBk^mt-p>8D!y;=YS4hB36vs{ct{yd`8UI)F(99@*{yA3; zCrXk#r+TqC+#Ran2|FZdZ~=X8G7bgN*ZGBrkc3yVZ@jL%HnDOMdfj=4ga5LN1PrjW z?|E|vU~FhPCEQ+VIm7Nn^R_+Vb%MA4-6%p!|Dh#tS6n}`ZEW?b3GL+s+;{T77G7+7 zL3DZ;C~0{h&7t&C+ccBq5I8=+db#U2qn^oXR_Q~iZnrI_xEW_koAX_y?=pyJ=TOqd z&aAJ?*K5DFf9g2taIg+Jy;}sVAV9=K-{-7Z&O5Ai-5Dw1y7U9*V{gRiPlKlDR{Vt( zm7WTPYNM>IVm5);ZOg1OGEiZYT3?lxlM5T9NV3teNtTh6{N^-4(e<{p1BW6+o;<_M zDllP0C^$MxBj({U=z-eRi=r_8-;U>KpRJj1R20c~+KQYe_@Kc=ikiF(H@p4dAUlPv z1u{ebJ#Mz>uy55)<6o2rdB?{~QJuC25rD!2(Dwie4_DifOz09{z?Ct;b0$L?3&6?r z`MJU|cCUi;Gd_QBT?Xb!nieWfDB0?GnbB=FON=qT7a}b^4sc2W?YXrdJN~Qg?vD9! zl^|%S2}*ZF(gS>FMhaQwC}F8h>oQ8^h6^M znJ8$6EIy`riDmw|Uv^FQpGOrN0Y0PpkK$UCC}}#va(NJ8RyJjtAC{4BN8YS}bSC+c zBazLjvL^m#5lPviK*=MZT~eTh=h#Ofc+c=L`A)q^O;qRLDB$oES z^}yU#Qz?aX{-suWIEZGO_^Wb}u{~q^4u2@4m;0U9zZ-ABD*QCBb-tKkNg9Zzs2fb* z!ydpPSz20pd4Ko=2ojN+_z~;f;(=OL9N8@0(EUa8#}z`H>6|=uS|>Z-EWU?zucz#2CH z4}iR5P`V2gC+0gps%!T5_s8EA0YX-lQBMpTt%XAX2@WiX$Itk`yKbcbOFF&9T5~(Q zsGdqiCxrlVEiSjYq)8REjM1_|J3>>#@xQ9ASP zRZLVFlfNU9;pB?rH0SFb(ORe^7M8p(wo%64Jwvh?e!RN+ep?t^CM{*&L3=^p)8(Az z7}m^}#qdnXW)R5AC_{r2bZRWhumW7j+}^j;KvWthlgk0j7(VJxkW)q#HN(m^F0wt?~4Lid;-GSa$xw((=b^9A5{3kteo5J zD)7AgGOZU09uy4z-Tp!{N8h34LnT5~T%mc2NH%%UO-(}Bqo%Q~9jwI>1N*(hJjsUR z8BuDKB6A_2Ik40;SY!tdH#2W0aJJ7NT!HeL831(Y`BPp0K#_M4^z*N+$MU53e-h;u zNJkb(n+!OTiF0@nPY+BGw_ewBjI)u3^;!lS%GgP?NSOvT=_Q(CO7pBq?=0EHtYQoJ zqPRY^Sjtf-$$wC0zaUlTjcEVWD#vjyJL~HkHD{4TvneZn-%b3Q#R6-N-lVA1zWTjs zc(|VGo;bXz3SI&LbbdtC8LSK*DE+658n$BU}ZE@X6i&vXqAu zhVV(!lZGL#cI)LQ47iV^$B+Xa_%pyhG?6jj-ZEggT&)yr;$8-plxvX3REk-`@b%E_ z{={^-(}@7{^661S%U}pVYaIH%I`Ljl`aYK20l}LnUqPeNnU8mp_jp!jVrz@UkkkVU z+$s%jqiPkEu)d0Y`AHB_S)K1SArTH!T`|kR>ae-p*_w#n0q<|pSXX{@EHz|Pi_pFZ zkuXLf;`D8*jaTg4YZSfOZu})Tz|C%A?!kq3=4|De$dwg4tm1>)GYEblckdi`tBUN=piE({&Grn3RAbk*n>dYyQrN!p;nv zEq+)uJe04+&EXb^luSl$Jdth_#WHg^DTTXQkE*0i%0w4T4xNJrjSg_4IL`dcGVoP@ zjVY^SNE4`|$Cst4>K7NWV-?fTiIsh476^bpWo+nUm;YU%@IDz}0lR+MLK?z!@EV%v8(XFgI6i}O{rarGnGWQfjIlk}gaQ7irAd}3E_>>9Z2(a+1tB4xd9M*{1eOIJME!-b^(8zMU7o9nm@#AjXDr3`AoYy4`oKJW z(>1rs#|vP)MkwSQ0wM{>Dk=E}##t=(F(Qkd_k8$+SB$_lWf>@(%3xj}yKLmobVD$$ z{kQW34{hufCcne#imsNGxj%07L_66P7C&p;u~bXS4D${Oc+Z8lXA35uZ^DA^`M3}q zL-EVB9$^yez{|?3L)$k~o%HdpQz$^Roci2gKWP)Ci2dZiAfHZPN(YhrN4Sz*aJdyi z&5f;O={(@ZPfSfnp2@3lvGCUWqs62Kbv7oQ$s^SO)1V!*28U%D`*{E_ ze144L@RT9{t@Lr$;bYumq2#F^L~LqjSJh2*|M1YWv(I)hiul(}Uw=ssLU@h4&r5f~ z>KL!j`V6kwpO`8y_jVU{N_E8m&_33T#h}V25t^-up?W@DziF6 zV?fE6@(?qT$YLPuMqE6$Fk4Liru6k3gjBo-#bk~~RgKO(DCh2?ltn9VX4!nwvOz)n zN-w>PcYL-{$;ITDY~HEg6IVqu1Bqiu+v@Ye9?25C% z>|BQjV}pOD=3*FcboO~POTeB`4_^D ze@{s~;~;+bW@s>sYv})EZV1ohywLE;h&xK+i$NernW2Of218>&A4c4TN>onXYzChe zHj15agyO$hq^#WExeg>bn^qb-B|%!*@rOmfb}S<>+MI4UC8eaiwvQ|ypFKk#cvu2l zFC1=GnBJgx*R1@gn?z`%324wa%0bKo#-X}%H`F0&6Cr}DPTa1SgdZ25iOn=PK_v>A zfbm(MwFMIz0?>DD0}vcZnq=Zdj>ZLqhGzTU@`Yz21i=KmBDM+K+nt{UL*_H7tYWwg z>~_b)EiV7yww`vRq$8=l4%hmHh)n*(IBba4&74@LuS1}iv>4>Xd}G#zov*Sh zNOLZB67+NaDdZf1m7`v99$3}gqIjNL6FNDa%(8N}*6 z@?>LU%!#-a_+w)+;k~j|y4eo!OlTNa zF-I`?#KGk0mjY(#uwxLRMn#moe$n6Q*_l6<^Z7QEK8}f~wBV7k+B{fE0u8e zU&>yaV9d$ull#-6qNa4pR-l}efcDpmBhl76K#^>oR5x>DP8Fu@s<6+h@H{}|NDcC3_ zx^-EYFJ>&F*8v}gEbrq~LQ}h&pMax2A@Dnv&F8WB*%qvsPuS429!SKId~`g4y{@r@ zz!AYrnT?S%w@(xO3ab6v5nIdu>^8UzO>%K=xtWYK5rf+3e5d>Gp*xe?{SaomvG3BpC_@hl2p3Z3*5H_q#2*K_Z zD+-~2cUm+7C-7PKv`*e^6CB2^Xr^ti!a9h@|2Gl)S4WFGGznFudF05Hsm^=SfYWD+;mgs3_vzQ

zQQJI%yZVQdc(k%;bdTDW7(j1|hm}uMs-zdr!ygx3Z||6Xr9_d_&Te!1 z0-6vYgpyHjlf@_xv$_H< zJO{=XO!AX2hLQB-yN|sM7Ez`yN1?2XZKI(Px~n?lh`)PRAu649MV{j-twfEK(;^*B zhd!RNrQlr{Wr7DnuYthM&P4K~Elje$a z9VFG@9L#RqM zt7Hln++mKV&nE%+Addw?3;*Zs2&$a#7yr60ewn-rARHQ_JDI&=viyqLVI|gNd88&P zj2iwz44=AnOrPI^^&np$jjXO#DUxjASLtf;yP^uHK9}?US$;50hIFsMRajHFM^dZ9 zKI$>&z5aS|z^=pGq4TOq&nf0lgg;V*|A|~!ixzh&)DD(`o}p}RY^dm2kQ{orS(>Tn zYZSB!GUX}#@vAlGU3T7Va1fl%(x%Lzjc9(az{C0gfT0H= zG5!9j`YS&E3mE+WV!tjn9cB=fBN&qTV+dE_hkGyY2oEtWeY|>O+lnM1lclxf(!kxi zM-gwpS;E}VH0NBiGm?i#&HhNd7Ps>*JTNSfgcnJm03cc{oSb1N9RN%g$lJ$|ue6om zP|-HLtY)(}H>qZ4?q2lVKb)?=`RnTG&9B=&xwPZ(@$uEBtv{@CFmsPIs+m&gW-`i|AXNT3_P+zEu9+Y4bO@39pN%(SILwL!) zMTZ_RyS@RZINdCN*ZS_!`5vp~c0l76?bQ1YG*2kRkpBHSwoZ|pPNp!h$}&LtRH{pXO8l&;!8_^DJ>( zP$Ri9ygIrLgsuT0y{fP#Zg!rA$;LOQXMZ!HRR8s&of@(;hiSHqo8+;vf2gkLi@931 zV)g1J?d?1Z)GHU6e=O3o2)6nkq5m3m;nLtzPj4eaIYs>XH0oH`_zvG+4#VE-lQv9e&zXWZ%hUHtC~c~S%$PRs-| z=F`%6{7s{JKRWE_ckapW67{SjbA<4W;u;0obzQ~D5oPjbCK7O>D zxtTB3zV#!H;Too=?_6D_mRSc=QAGQvq?gG@9@I702gb`+NzzZi=u0JdfEIO zrSMua`gr;o433!o?eqY9dX$L0lE8MtvwNZ}p*M6qrpP2lnG6%EqR3yr%8J5LZL1#$ znXq_Dy0m|YX=B5q8PgFxEh^5=U5)H!u5`E&^o>tnuCpl>MCG-YN1WFX*Zg&2Fm4{X zJwg;(QCbE=#qvu{I>nZWjGL*=Ov4)T4ORa5f zeFo@(t?o_)j?t!7qf}~8dV)n0?2zv9odL()d76w4{Wkk28T!`^E9z9_d?scNg_g2% zb4pdbBuq@UAU@sHo9kZEVl2EOTPXl*k!M3Kh1xEPG`L8FioIW1y_U|A(Ko?ny}Bw& z7wt~j&=?&1*Nv`Ht9E~FqdPa?is>B&tm*+&eqVv65RhgL18x)GKmq-!mWnuT7(lhu z<^6G|V`Xh>@19Z{aHH2&GY$-PfY_@sz(mtdElZwiwA^q&GrK9Pi;|Z;U1?5Mb^FCe z1*}q?IMVGpiBJ^Eg}(|E?U0Aam+Gq&R;YmibyTd zM$Aj25Z7f>EpK?4Tvxi87jd|fuL+I5AjZXgZ1AEk$~IyKBh?qmWvj$I!QP>ZX~s6}f8c>*Ag#J+M_0*p$_Hn41DuOFIGc zV>b>ar-=M6vAUC#dHx@vv+fD)NH7IH9tVlrFiHaeq{CwoL^X6)DW?SI~~sgn;y+|-gK{k7Y#z`#sQ`QQUdey}AIXYc3b@k?;0N-XyIub+3Y#FuB<&JL&>`2LiLWx3vPZ6t zBqe>74{WPYEm>cF!1&RpZ>{VfHX_W^)CakG{a4zUgo~uA5J%(74f&}Y*?*H{wEyUp z5AG%zh(g_qgvVecycX)W7}(GWtsRMi$=Rvi$WZ1f@%3eP^g}s_b{y zwFnu!^Cb5iIl_Tsu;i2x|G2t!)(VBi;PT<}ATD}!dc=>tBRLUL(Qo}S!`mSlEi834 zXlBr^a0#$lbGtldAryUeys4%-aiu+`&-Etd#AMjv&>LOex)q)$CFIAdfS!u3|Xcv5H5!B>pF>U&G zfF$U)A#~6SbmPeb4gk;=i-nBCBiI1}xPCiMVeeLJ-}WDRKug<*3sq8_E%O*WI3;Q? zz_3l>(-z~{5^-i|OjI-CqC3~v!C9~z$4t&oLhc9=P*^V(v`*jWSi#XL#nI4_f8lfA z3!_c{GYI?zV_#LRmQ@c2sYK_KD;t>e5jd9$kaL{vBI%+`->=s;$a5XG0}_L-uAI8B zq8;i01Y3^xF%972;RS2z8jT13A5CW&Rpr*U?Jd$>($XE$NT+mngLEU^-Q6rox|J^J zknR$Yl5V71;+s6F3AteKQ{`6it9Vxztfz3q9(7Bn_itCUwW1wlZ!v^lZ~I5S&(3=nAzk+ zzr&=nop>{|M8{_jJ2|>_2dS-7Zync0k3%ZoTq%o~J~#OUt5Yw(eet*aGgCf}dipmx z(qdWnJ!&?QHyd6aBE*&COWy83w>_=r9C_Jz$SZmDGmBGbivn7>OV+(Onm^1c_x=9O zQCArtGw32KgJC76Y*j$(IU8`&@jl?0_s2QNq2mz}nyj|Ex}Gn!8zrP9f!eR(Jfe=w z@c75IF_1l2d%pQ`e^n~vJ8WXWcl+$tNbCP0zz52%a@@ zffbzmnvp+gIKDfcwbnRmAtv_r72&|3QSll4%4}Pn3-$OP7zZrdeRMOeKFs{_q%N-U zmvEk#w?ew?pccVzEJ`vw)@tG?GkNx{rS#bvr$5wRFdK}^PQxAZ#0L-6GvC~>0S%Cs zTEk#@IqG`05xgH(1S(09`6_)`P`C$uR-dzA5}#G?e_bzUlKg8c5m$PqmhLHi-D=~1 z1~2^?5=;IwhZ_BC_4Z^*0TDmE8$-={UXVvxJe$eWCW8?MI&R!-f8w|I{%14nHQb8J zzdgQO0kw!@kcyr1DK7d&WmpTr4CUV=zMa9P*)Zq}sAcyl{S{(c7mexlr948c{f83@ zAapXoKFQzvilYIAr8k6EwG^&pSzTN&NB&RMU^f!Q9mjlBy#|`%_5UVoo%h__PTC=h znFAQ_5K+dPGtA>i1p7}`Kh93A4Ex_brtKe{H6I+>!;Y#FeNe!%lJB{;ymbD=+mnY- z6wFMOvz6q^X4=6YcLjM+EX~sHIcKE~Y?iTe`){yO!CoR1yJ*zIH7ZR(FzRU6u}Ca= zPi2TBKZ&3vulwd%w+teI5Ayhx(8-PD?^5#u1Fb`)Y(``p&7C0KxA4Vc@;zVKTq!Cy z<$hZHq0IaDv%N6DK4t@`C4pxQcJ21y__zJ){gN>{-VkVks`-MwOVVW=aBwBlicJJIbyG$6T;95cxjZbOY;sLN#RR_o>q#c4<3acdR(23rwEs~`1r_1-QGTSO*DCtDf1uGec=LNTbFO~L#FejMPM~R_Zn6 z{o{P|2RyTGH_}R^N_EbB(29v-YVdQpC#Qa~CM{XmsA74e5*4<2ZndErcF;vm;W3UH zJ2_!ia(0p=oNA5q4Uzh_NU>q*e;kxvA9QOm+9wMUmI*xdvu35cG zjW@l?iUK{^BrOaY14u`wWM;n@b9F)15r~DP>&s!JbcM7a zxrdNIt+4WGyQXX!Pu3{tKRT*!1bIJ)RJ3`eiI=<5CZEP8KQD8=rig%~ZtF7lg&*)D z!89~weqKXwp;)Ed;qlIKzABa)3oOu?y;sdGEO>a%2>$Wu-XC?{^Ch)Vmy`^^kFs+# zy42YA6WBjVVVP_d`rNj?bXy3zBGng@%d$lec?()ZQ6S^Ww(|;%E~>!j3LEl@XPM2W z%V0jRGuqgS6NH+z(C!!~a5sq+>kPO8hYaI#51vRK4CF6)40*OgO$AX$B&En_;o;x6 zBL&8qQ|C6V86O~AH?#T~`2=bRtZKgTMaiF=^nSWw5wXGT9~ee8V2j{w!o3@iR8&cQ zgZuOBZTXZhK2>$7=o=H{j4{0<5%?Z3p<@NT7^H1rB^5j>An z1mSafU^-z~^hj{H+&~eba_Ef(v5d}?@MdiqiOdb*XkH%;?Bd#ob8F8LY9KV+5-~@ zHb?G?54(QOW>e=o+aY%+b#n}NWjFBOp`Q?bhuiumcvG_XQ)FJd~Z4muv;vA!+ zP5ZBo&7N~O1Rcm7)a~z)HJ1d?{k4k-Y-h%Az5he@VhsU!yWgNz$mxlA%8xrT1Nnu#l%#4 z%CUe$$izIgftO>#zZ3s;-ybNewI*<&H{?GG%*C(F571K9Qo!7y#3Oz$4ow^mr!dvg zoCanNN#mxVZw}%~Sn$YjsGXv^vh2+5HQA6736M@g7lj|78NkDT-`~V!z~__RG|%K)AAg)}PT(Hw?{Dv<-6l&>;t>!VdwC(+alCyiX=QI^1}3Jj z=Dw?ajE3oy5vqx=g7P2IO!lg94-OA^i__euLGtAC8?;}*o;nZUprN36-gLFc+fw(I z&z9DvZt|@?vMKPsgv})8?18(==6O003xW?`f7Ck$4=|D)GGzk+4X5YF`;SB|%k>s$ zYXUAQxXfQAP3zNpj6&>0Y9r{>$UQe}5mv$|W3hT0w)Kz8>72+zNW0|}W*5@?GIC1% z9T^T+@Ul^?c)zQ+GK&0Q(gs(?I-Y1gww^F?&xg@>Tswrd4C)5&i3S^)%oXD6>N*f! z#1f__tr=17OsAa^*ld<8C-8&DdA#!p= z9TA$A$4EGws*j3ThJ}S6Xt$@^a*fUu%Pw~M@^zjjkM6uSQCjwLOu&!#Dis12iVl0E z!?TTE+dock<6St2_)gZptV?%Z z-^FL(eY|q5>WvB4TkA^foHAmyns@zR_Hax|qT`9OH;HqgT?grLB;lKrp;i4#l2KCZ zix94eJkzpGAD2rgS)A3vqHWr`ojlUS=HL}Ekz;o3dC$q)>V?Pcm-&faaymHi988hY z&X%T=U0klu4s9f*EeP?5vy6QTq&_T+gkGAsxL%(xeSce_(;1d=fT?e%FBd<#2_K$w zV^A{i&34%Z4({fC;SYa00RaKczVW-I+vhMjcO;nz_bVF4)QlzX7*Hp5xH;B-1x63^ z0v{7;s?7A<+&gU;68<58@d!yt0@*!>r|Sjqxl@vvdjXHG`bojj^R2;_>v`Q_^ZGRT zfR}TUmqZYG&|5GA9%0b-+rwlEb}yE{H<0b>1jKWY{s6b)enV4`28oG}zXYI^D-?z> zp*!Z6j7Cj&qo3L_*>Gg|**vkO0uM9VfZaUMf!LG)&KkixNscqvPf!$dh{Y_XV@M7m zpD*XB?OX2~D{o=Ke}gu7BNu_G@u@BmXD&}}jzK)m5<*Bpf|EZDk6f`TBq+L5P-4y->3OY8yc=1A->J#_Dp)Y zT0M2-$m*|>Xv(7J(8|woK23tMLx>Sy?d~3uk`>5EFja4WrH;!j^|s<`GD)n3)~2$m zXHwAg)hO2EugsVyE1pV72(n)=T0!z?TzAYjrnL-IN~je#4UHi!lP^#Az~=l~j}W_L z?)=pFP)65x_DCf6#ruu1b(o4{LjP#mB?uJ=Yii;e;az}g!|UA|huJ_5I6`FTfGy(g zCvIo=tKDRgwF|so9gfdLPMZJXzz&WaFSa2qNITsPeFrNAL!*C}d{%Zg6v-1xj5v2) z&y$8bJ&J4nyRi$GgZ?+~Mhu3;&;n(Vm4&8k8W}1wS8W%nzb8wm`%=~n%li~}S z5vz_s@Fi9G%8oaIOYN~cWpgZ1-@-cH6i0Q%=|)xwJzyENGpsd9PWrb*`ZpbVQ1^aB z?^c@Qu~g~=8aaowO)v>`NJ$I@Q+tifTs3%Gli9bP@FHhg@2evF#C_%Ny)auasJ%LB z@_%y7{MhAuu&_A<{qf zuRZ@=>oo4E7;P*C{pW7bu9HQFCgS%v6Hu=3o+P^k`o9{WdU$P#9^ z*nzAZT1n4(aKJd>Sfo=>thWQ)Vn9CD`DKel6dt%rKn1*O^!e%E{Y+YC-)q(QA;vvl zY5mCX#mh8dBW?e9pE7$=H672?c%QP|mTx4Ii+Wjli*mPu7X{Yn1KT-kBVbRXJvsS0 zf>qcC;x3Fcs|Aq@OovPHuPkawQF{9&FQ&=AE92n#dDXhtk^0XlW&=>5DrF~ffP72n zE?6iJu=1*?j`GFA@3u~T;{zw$Lcm$NCWA7)mwGejm+(^qb4Et? z8#4SfS6D>Yp4$5&Y%&&jH$)67q}pR+9Yj&8vcBhC*Pe8|Z=9S+O1s`Jlflj=Z_1kM z%Hb|8?T}Osu)h;{FM9fgl;*BYSO{KAgNz~|y!u>!H{1cr+G>y842%7vBMM}(*UVZS z@B=!vtWXVYy^kb#CovRi z;!u&a7F{N$Rh$d>E3vv;hxim_m!~n7L(xU0BJ)bf$eArZRvpWQg;i z$f=pvFw~=Kq+dlr^L`$vHbw}g!p1UqM@6arK1nG`_+Lt633hxL3`$rRUu$;vrw&&| zn}Kj@tGJPKLtM>d`C zlr49rgMfo5$(flu33N4cI@mX}2KcceP!tBjJ(d#DVRcmnCFasnmadee(e+ow`lxdM zxCNfR`aV+vU$}?i#?9+i=Jl?iQKctsV{-&0ssbLc018NxJbbnimj(#kr0`0cPzzNC z(IzyGD)MTvB8$>>mAiS zO5;nI&Uz+AowufmXj!y`_2XBB#JLU{u3b+_A+RCtqF25T>6XH;rU-;g^F}2(u$Ox4 zU1FANBSqckG8l|TaO1N$JD&>=H*sT1&6i^NoZdg_HQLgFpdE@R{OgDX2x~l!YX{Hx z<6l+SJK)U%xJnT;IUYd+0}>^rrC^9-3QmO~MpCl{#1gQIjz*@~9sWXllewjpm9fu% zh`pBCBgSnO!3G+1xM|cn=wD7L);1rnxLnk+7kCd_PoU!7xxWnV z9WtfAhO&SC!ro&p!sB*GiQ*k8n9vZFMYrtj6of<4I;3y%wPf0hM?1fIc@X@!P`z$Z ztvlIJElo*q4(y=-9D4!pZC+At8H^i0PMtzZ1w&VyAWRk`xgBBOhM7=tz>Vh+x*ejK zPhc5?F$POUvGqqcnuPp9%fB?72^y>&J8kQ%V>T3Dt`x!fuB`25)%SMQ=cYb)C;oXq zo@L;Sq3eR-g|dCxpMG`T@1M{!&GX~$DWW#5F`h>$3R3vP;lT1rK~~&88~k>R#N%%` zjSFR$nG}iXDj10}%pPb)yKFc2?|P~FA?br5)g5USc3i(0U<*G{TG2(!t&%evQ^7Pv zeUJJb&`tl&a+o&LS=uW(se)=)7bhC?!&3&VhO2GgaAV=IsT#~goB6LX%b2*w-nU;w z`VZ1d(M83nQAL&Mz(2(?3q(yT(%daa=;w2}a>?8AW)l67lMwQ$unMY{{PK)%^N*&J z2!8gwb=J*(qckUbB}OJ55R)wDcBL`0@{D+o>YS`sb%Kjj*Q2b`=d^<+#Icp3MC zT{)0~0=8)4>Ye(JdZKE-Lky{|%72qF>=)A3Wkogq|q< zRk=q4Ydzk#K&c}SEBvEGBpyLa5Q&J;eOH206q_$#bV=gBm3Eh0{e&_IOH=6F0GQ)F z@+B0B;iE8@T6g_@CBG$xTNb*GJ90mou?RU#>Q=bwLph&&jIR94!V_SJh{{I7L#2>> z_7s19>Uw}aTBsC*?*SQN0HO&$l2-mpi;W1t8A%GIi;Xt8;3GQNWY?;}ymO%0sc6C4 zUtT}UcyM6W#4;Yb1$V!b+m}#nbb};No5)MWunsdT)QaSI>V{19TI{C|W&t@H#tj@%v<_bln_Jl_ZSH7b4$09K{2fD>M>@v6pT!Ah8)1y%g5T zri~-QpHdy=E64j@s^D}2)LMy$Y}hI!B0VKoN^)3!`Ex=1*l{(t8;e8+=)Ok;eI$1h zsaK~=Pzr{^I=?;E@}Vv!&2X@EVydg6B%^9dNTiM9ih;<=<@h0JE>fmMqbKECN6M60 zIb(A0N)u#0)O@8}V)#s##d1}SO;dIU6E>piQnZ9ujgwKOknHHxTrg6hX`H(JvrEhW zqkH%pKQqW|a`59M1#@CzR6OH}MH8Xd z76z-)_p^JWCe|4-a*xk-$kv!oBn>qrSRHsznu}In-CFtfu2E!Wr>S6kc03A0A>#L{ z61;p=KwWSJ7`S7z)4j*ff~>4P$&=3!`8y0p8l^uoqa!iAiZP7vZp%GSg%E)9keQAC48pL)g=tRchmapITD^ETL)zr8yQ2Tr?2|yq1xea+wr7p4^{_9)5_j{V?MeagO`^VK`ghpBl`Yv&p*B= znR%++g96a@_JHIaq##P3{Tg3=hD=#MYK*O644+1YOrA;v=S!B(f++VS2N9IpJ=TdB z<}1l(A;!1gdt<0|9Owy_XSD+3meBHZ+Dg+_vYO#_@CI>LU~7f{=qNFD_48acxB8XJ zI6|;Ja>DmKPyZ1BHjDF~Lg&-pFN4WUHK26zdggg%N7x@AeTzcRW|xlTT8rLq%}DhQ zUD1pAICebv_Hb#hajb1V9t-NPIz|;?kDDuPd`rvr(&+dy{MZq{n$ASeKGK8hr9t)0 z5^vZ`loP5O%`DeO{7ePlto}KgVY-VI!iaR z@ypPZ6us=x^T#uEZwmnkB^A~mQb@>-RW$W2qrU!;LrmBw$UsTIPiJws>ek-xnJ$F) zUdZu8W()UEw6cIta2_~JgGhGB3#;kH!NF0pvs4Mz3maG_Um4`5k4tFfvnf_rg3(D< z*x-W#0IRf%j1HAd3HVf?i;{S_Y_%qFU;gD@0z3G_dwGJ_Inu+`!RUnCq!1VZh0dnr zv05mjZ0@P|+KQ@F(yM(Aej_V6f)1sO8-Ok%r7rV)_ww(9v~1#d@iZi~>0SROcBCEy zVjv>aCzQGcuPV&=2e)bJ-wPvMk7I*($^*T)Vvrp#k8`X1*f7u`$)UF$z5h;Qqmw_~>?TWr}4~ z=U=nQ7Ruwo%YB=z{~?^u<1^e{@5_ZD4|`4g^f|fjoc33>-kqKB#@+kUF2%_t0F^-_ z*&ylsCjjQL8}@F4F;Yg?f=FNfFAG4m zq`@`Y)gJ2K0vh^^0{uVY&}Qm=5aMHD;SEi^6Md@Q^{oW4SToP7-+X`)KKy6d(aq+J z+142K!W36YcfR{{%J4Veu*;QU!EW^-u;P4>*gIzJ{rc>$)m){@KqbaPq9Q6 zA7nlp<_h>#>L`5_&675whuXTwdlQK1;dJ8K81PRbMyX)Jw{AVuraWgjQ}aot zYNW&=q<>K+l4_Bf%eB(vdP+NP^6M5DbcQjQr6A*%W1qES#wCBVS#8?kv$EOmwj}9d z3!WA{%7U;oJ0;{#8wH>-mUU&1sKE!SMD=I;ss)k7$e};%4E2oMuR{P96LcKHtG024 z=?O#n?o_Yb+}pQmd%$7vf0$F(BMne>G;mKoI6akYx2$c_uGJ%?_8kvp!%pdTrliNv z7qXL1A(OR;^y@`zye|sP$^XF>r)te{%9I*om3KP&(tR^3;y_e?hL2h=_C+q zqfzjdq>EpyKS%foogvZ0)+eZ{DuT~xJ+(v6dsU-O-frdJw7+r}P*nuI#@+xRsH*kw z8;pAX>A&B=V`nx>{>dh>$i3IgfSCJ}b8EJ=KVm-RR!;cpT3E;4e|tCavvZ5%Qmu)k z6y*>>|fS=X#}n;{4TJ} zkzh_b|DD{v4?Q|Lk$^wXDYt(91?QrEF073tzP!h*NVZE1xku83k4ZoZa;ZeD?!Bdd z&ZHF%Q!!S<9!rh+It;CJ)4foj5b_<%#+d8Wr%sn?eg5Hwje2^H-b0yUcPu%&vLh!h z|M9aMq6uGwORK@VU+Hc85j7NISl5SCcyKXHhy9V&6c&JUF-*GcQ9D-W62;k~XWN>z zKCzR9_tlc>QypbRp1iPR(|fsyX}^Y8=}_#%$Tn6exuNcDm=D<0R_=E8Eq`%{|4rP2 zbm$9Z1o>q=5ZXD^!i(k?#m@JLs_ogCubY?@HvH(i)(AQMcf9-~tx^}{PES7eybsbyxr>(=0 zi?Mszp#)Iq)e`Te@hTDCSyxy?*Ah@A1P;)u?-fBx2c$WgBp&uvBueNU>j*_O&lgkz zl!86a8&SxEk zT%Mtv!s@BSFJzTYM1tpUfjB_GceMFBF4t%boQ3FDPO^R4;4GY3%7T?Uh z5{vVyuCPS`NvRzE&{W2R2H)d2VspFbC@PHn@-;rfSQaQFXcZe%y!m80k#@s5u9CZW zLpt){A0M8qJCNoosGx!2l2BJD08w8?T2T?L>q@puEw{_C(fS>l(Bqj9Thdk ze6K0$zD3S2Ef32qz*Q+H0Mon0sKIWJ#QgfLsnt+OouCDSHT4H4gAbUK1q?j6e2n-# z!W#S#BWaQSkHEtnXOs8?x(R){S+_z`$Y3tUE7K`PE{xpx=bpaSwv2|Ut(S^6rpi`E zUMHT{Of0Pxt3F-<=i(>-TiB2UODxO>vy@cQU#yU*fe^vK$dJ0w1hs~IiiL@r{7Zg5 zBB$N*u8kZ*`<=PB)pgc$-*Q_eC2ht5F#iLC0jt9%yoP&3){t?VHsiAIpgAMNtb&WLetFNDk7wWh_3O+Zzec0cp6?)u2^IQ*vdyRPCuKD}xUmtGDfC)wA zM>{H8H*Vcb5kl!di4|jnIx3lZI&3L*8T5E($dOUh`Vl%NLFrG#HrxwoA*`m$1Qb6S z5bEEeeJ5Fk+^QcrzVa)1iAGGx8BN!(}KG+a!L$PJ2pT=2^iFf?=r2WHB`J7bS z=G8-bWVjH8uwZ4rXG3wFixM;HPV61#+zXg=0H>$P?du1>fH>6j<02P!;%us`IXR@lI5Q8OP{(=n24KZ@>H>ax8(~q6|X$HYpDHP{Ou2ArpXxB zx?@L*vLcoUq6~-gLXUmA&3 zt}3Dvm_=$u7L}Vgh1dyd)3>TBWEMU;GanB0GDEN_nZ$#4NJC*pBnWz3&{?#qHf`iB zk;%eRR4PV4PA{%Va;3r{8`+kT|MyOF1s0t1oG{_Zl<`*7#8w3g&5u93M4B+w6EO@{IFxFEXKKgw(LQ;dcEGvNr5d!6^=U+RF9Sr+Oc@NKn{bWAPVsyN$R zMDmy<%S~HKXxD1LAHeU-S9h`DuS)evkcRl_Tg@}@{AAg_S^h$kDsw)p=vVjrglpgt zI59B+6XgOioBs7HTBX;tj&498*GkB=<{N$`0V0`Jx#jpBOsxJCcRzjL5&CZ&74bfL ze6f8AcvHL?gKO=%Wf0KgcejopH5xQ$a7^Gbf0LyjqWoW^);zUw=V%M#;xDL5uIbwh zNrUXY293R;IfMpdR0MYFu&&*FF5K(w@%Gbb0%KgUXT=jf9IBse;#4eonU>l!LAkQ5 zG5MDd-#rfYwU_jI?QZ`}Mweqj@$QA4kb6s%QYqGJlbeN6{cG4e7CC{>NPb(Q1 zr2+klSXZmRfE)}92j>YQJFnl)Av4wE)hGQRhbUOq{;34&tbe%sw|#S95Xg-P&dSq3PS_B>pN-85?P~$ zWws>8N0~DD8z!-nCLedG2c{n@7lG>gjgWah?fMs&x2+)uC6pqfWJG~m!U3ca?ACO< z^Tzvw^HE{EEpmNgiJ}2cpY5VeeN~}C`?wHArGGj}(ZWgU;AL#OluOaUa|`;$6;U0? zl++fDTNZbxYZmv)n@EckCU;{-orDqPP@S1cb&gBmacdxQt8%3Tl@(COd~hiH%p1^P zHl#DP3-6VTBOfRJeQPGuKEJRfet4K}X+}GfAPX;-JDLK#+Zf7^$&b~D3th1TV&aa4 z_IQF2Fft;3g~;P@y-;MSvBz_(A@!-~T*u1xF+o>y4xlOX;DPPqR)vfHr{MTi-gCCW2Tb0fj}O%L2=8DQE0{X%htgE zUDx^dmn0zM`8N3oJ4|Xkyr6aITGlS16|MY3r;#Z=OFEZi$|k-mV<9Z-H0R?Bg!$8d z8GFfsALps%`pR$}lk~g=s=0%+BF(R3i*bY>UB^8nF?hN1O~gYZn9d0v789DRPKGWM zG`IV{m$vwAIcYE+l@`{=`~1UXyO*-a&ngBBRdy~eiF_q32Scz9lcL0QRft3nnwU^{ z<@kalC%uG(0y~Fim;_DFOeYs+iyu&y+-IPD**=>>_x<}MCCMhh8FBM=0>VL4N_s`! zx9v$2*nR@*)Ei70mn(dAlds-~_ASnk)bcb6k=_m{X3N!|p690HJ;`+>f@r>f!FTf4 zq<5jK(t_Jr6^vm8r?7Z{mzz6X150;L0IPV;fOdIXMI!N$3|cwzSMJKx=b!M_ae7J6 zsMAmok@0SRzkto>X5@`Kt)Y!+dH~tIX32Swq0M*;YFwEUO|;%8^ho%jfZsE&bM&)*PhFXnCNkrFz&CprIkUD4N7|< z3O#bKYza(J%JFL3EvBdWf~(co-tZyMBP3Uc1~Et_zJHeL2<-r0f*dcI9KN)^fv$kCK&u|b)i@6Xz`o%Z8F?(+ea z_nc5J;wm0n@(yiblo4+afT%(Jj`pNiy2LR<$rS6hZO^M0=DPXxfnnH2#$hNauUji| z<`;wg8xy2YqEjj895m*`EkXSVgzq4=sbbNELp)YE7WI1BI-!Qy=JU+;;?(rws1tJRo1VxT-yd$4N)1!cPPYr7po1FjPsm`Vf^vaT)*4=Np!jFp3C9)agnNC zwYB=p0)w1c8g(VURKse{lzY}Z`%{AqxJnQpRa4P{w-SGNbE>*n)o5<&5&|N)CRbmJ z9n@UDjo#dLTp+)dM58cKy#C=$i5Uqqq5K~Zhy!)vXpWHXwen0p=kl;TAq=|+j^?uM z;u%?xbz&{j;P2}`G)vv&t+h&PYC0xV`FGs97kIZE#Y*gY5lJiU*fBY@jz#$+lAUL8 zzyBhXKy4nb=3VjkU=~ExQWG-B6^(x|DcC2{BcALFzl37Fq*l2i0)XkrDqsENWHVIy z9HA&3)kIZJV{4}MtTKNhdXaS*IhCXdDK7%(Cwy^PmFZL$D@Dqo->CBT5IJ&1-npMP zl9M%VJF%aUrNIf~kK(A=5*G+Rzp8cs4eT{hJG-zj2=@8_hzOFn3C6X}!iGLBF4&`W z=@~i3FP$V)DHEGzlvH0JQ8)0h8{4*l?fUxdRW*I5hGZqMu} z&tOR`Orvth5JFKUl*6jgl`)u&-2D-En>CMvC{>a-ibx~*ms~&?uH2)sfBYS=BZ1#G z(6u?0y*Qg)5{t)?qol)!$kKj{{BLO%wS?tok(8>s{Z~)+FZ`S2)Hc5E?J;K}U!nEB z<}c)R;M1iyfVX=sMLv2Yei00lRp=L42hwEpt0L%Cgse)Cc-z4=U=jP$nF>*RJ0df0 zlqUGMzNvV~#T61*jxnJb)y~f|Dl%4!`5SnIn(w)ID10Fnokk3An{a)p^x&L2__rrRSC3!bTsv#xfSo|h!31JO>~y+PZ^HSG|seqJxg-9 zqS|WaVU|`$<<2ax6(Sr?ST02@xoDp(vF)=r5!qPEOev?bKp0qH5 zQIbBc8FiJyEs41>>jQP&oZ7ad3sMWQH}S$ z+Z#6q)sNJ2P0>O)p0u`xJ34uUoNZAn;=ND4(t$=4mTDdY_wbn6fXXi1zUQ1(WmG&_ z-9yXeI*-V&e^s^fw%ubYB)(X}tmeN7WR+^ERI9c*H7}li2@(2al|{#*|WrB!q|# zcx^fvp#Lx)Y>+Bu0z`Zve74w?mqv{A>sYtWtYgW&HoBOu9bD%X= zSF6DiV{d|UbHu7BfpSG6q;8llyy^y8gFJkvq;{19IN+e&zqCOjd(y0AOBR1EeWF6f zr%%JHkf4@pLYE>N(!r5;dv$BQXH%(k=+rU$g+#Hy0o|`OYpbi5s-QZT8IqFXI;7NZ z!Udmkjv^4(dr*hVLlqWgG^vC@_C*|ez6Vh)==T_1Osql0z9+(#hpF~@A8KSY=2S)s zqx2HTZ=CPW@*F!j$x{^-F@#Vip89K0?@wgZ!p;%2&u}CaF z!2SuZlgvfv%%#n@zT}dmqJDYXYeqkD%sTsE!Zt-_9rF2hdlgb?X+g zMW~o&qOl2e^)95d`dgrxq{AVff{M%vkNM-WOZeI%sveW}eHo)!u4rT5Q_#k&n=U!9 zk83G788zPYtS@ofXWGpF>FHv^xf?jwRA#d3^uGe}v(iQIhH<4XozG`+V*zxD1P3MR3VfuR26ms#n-1cAbvYI_=lOESn(7}B zG5JdAR9WNtYS&9EF0rz6J-${D9bH_RRLniWR-Z)8a{ zsJ=jR-%Bf-A3|WsH2{^)gu_SIv*e?H-5r^{|)VS z4^|zU&bxl*i&xd3Q_I*w*R$*Mc-Q^M*Gms*Dh~F?zT%P0L6Y*-YZ)0CiRN>-X9wdl zdFk=Dae)>m8c9Mgh;%(w(|`9`|?iPMDoL5q?Bg==vD{iUvK6vQ-B8dm;!nA#4q~4#0Z37XNOy2BbRnDmu+=ZcE#R{x2hoM%?_-ZbGLc1jA?!;`(^y()*4AiO16mY4WoL_!*g zB)o`FW$26b7K_HyxIoy(Ia3IKY4ESbO|4nKhDN1MsNM1_p@IMUm+Ir=@GeZh7rx(lrh0d@#y?G^MkUpAuTyMO z&8bz2u5~_n3O;V=8p4q(Yq9 zG&wU*abTkZC91>}SNl|)R<1_R*!4gST!k2-=+JLu`DR=i8GGtf1G+Wv}1AXP%m2=?B;HqMvT~6NCLYVHktL3m-S56FZh#KVEv0UDtdW;(G%Xm=tv) z#~#Q^Yqj;8uE=H6yG)qA$SDy*xVA;x!=8_uU1(dhLy8_E@@`b#gvW|;FV?$QB77_+ zccZ%NQpjVSJ-7GvEtqXS2fl6rkE**m|Dc`j@2?ZC-3=VEW;^U=!AyVEzqD zcW&LEQVHy~~rFs1@#+3hHvYz}0TAhFqT#;+Yh(m`HU7_EGQ^~pZxciYVRmOIu zIT18|KdgV2hzvxO@CYsUh^yHLY*@J)CnMFRJ(0C*#?u(3Hw3r{v9JP>*h`B|om zuAAVGf9l`znci9?ix=g=w6bdKW(^9oHZ5C6Bu7$X4vH$`LK;xo;^|7Hz#tVMG+OTl^1qg)7cS0MRxe1H zMVFR^o!vPy_5>^>v|8NgdUweGj)-Fx^rBWNjn-4uLPpM&(>B6xEJ z+0cb}s8&9IBJTkXMew`?!?^?(Uior09Tvwr;L`?`rZ@J#XJTW;X2w}{iDJ}PSNYj! z!Hjs?HA~jM(1JJf0PStBtr?WzKgz$1XcbzqdM%J(S#P>#1TMEKQM&LCW|Df?qvG_q zqtZ&(zRxGqlK3QqKzUr*xKlvXNF7pUu`ELuq zh{OgP?!3Y_58*qP5t}wid^v`hsXP${GF|99mEjWJtR^A1^iDaPtY`zjdR7xEdQvy@?99?@#T+meezf_m$r-&0^Gx=TK-|FiTQMpxf!B~3 zpOk54?FzO%o)9z|I^l%&Pv~ zU9P3s@!t^f3F-AEtD-36g>io8P#sZ1{XIN7<%yL`e6?vm{KB^})7TxD1i>PS4R%Qi zu$tj`Sfrtp$e>__fT5-giB^mBob*ch6mEcTM&p>IowCD-d>jPh@UpY6*TptEG=;&a zU*f%kgG006yuVJi#0RDDmAeO+vOnct&U$9<33xxw=$j z6f~m-k>AQHpG9+7t`IW%nGwFFlD9hL*y_|5RUR?jPOZIH`I?uX9|&ec&Zix>od@?# zb_>~y4fAUBsUPJE05K6Pknc)Y#j4flUxd8B5SuQ#E_nJ)@VI;1oMfn{s;5$wVWNnH5!okkC|H zE1F7{e(u{+{WrFHC;ubg+duVVBzwD~^s-Hqvaql|+9vzYoV=7jsEq0;R9xWL|&|2FHfhX_pUz)2DopI zTN3@4&0K78wLwUvKPKKnO*Am-2yY8d)~&9!X(EU4X6o;?$K-Q|Y4Dr6E27Ez7#7r) z_D&f*aa;Q;qv4jB)omG}or`WK+u}(r^vlLZL6uM!SQrIPj6XlHsos9l$P4<9XDCrQ z;1^Vj4Ma}HRD%@!2cFR6H=)y=zGh56wTlw8Die(&MxFJD?&Ux@JiX(-`A@Z ziuvojBD35LHvXAcI}BcZ$lSoA*qSYBDOCAFu9iw$o)-kaM=4E}zi>2b$q^<9T5)8> zpXmBOv*a06&mOpxE!bTB6HIQHrx8@fPAs9j_KH;~mF+N~kcSh?-Q%Au*TB@NM4@+- zD(#TQ4#V~gbB3k*%raC)iO?aZDlc)6#qbsPs8?aaruKrLX0#3$x(Q@nhnTB-JUTSh4(1O5tW zOs`eqMx9hYA*wjhn437gKowJQkw%Te+}&S?D0S_mQJ5i0mL{Mtbv65Dd>8)7MOdX= z<-0mQvPzNKb6b^0xJ!{=mk_^3OOqBxfrQvAJD2d!S*0?JA$_`Gt4rkN-sI(X35X0gC+n{|Mcx!KVRmCSgP_6_BPQ~`I%nj-j# zU3l#duc=$yq)J%^5{rFPhP1T}KYb6n_Cv$yt0;wV!}D;=6ct$a)fTMDmTO?ORua>z zL4ONUdzWM#+^||;P|ZHXH1`0n5%MXOKv}PND$Kf3qI5Z5`ODPNwNw(c{$Xmk(f_0A z9D^h6x;Fa6wlT47Co{2a+qSKVZQGvMwkMd_9ozgmZ`D_o&d;Q)`t*Iy*?V7W{qi;r zof&}$Is)%Bw->L371cu?W&WPv09taqmne9(1K;pe!`-($fyB8Jn62wlP8MEYR(S&7 zu&AuGlY4ykODiUna1lundj*9$PHHDvbl$bIsA}6k+#c6NjSe#Ga6B~_#nIUCufN2Q zMY09_V-p%AD^nnsqz-2PDHukN9D|nNhug2l5chYeAARJ9r1|+i@VVS1dP^&!59R^= zQFg79r>8WUsfsmfy~I?P`U^_HxpCmn_5>lOX#aO2`MT$+JMR0)few-P^$z%MivlgH zFSGu+oV_p8Y`6$O7Hzn`ShLpQ=9M3(z1L!vhOX}6E|3w{KXQ-sb&dRH-n_s+}GQ>y5ySMlXT#g*QxiFc{Ow_8=T!@lgt4b>1*S+UU)&^v?R5)?&;ZU<4Yw7 zj~6|GcD%+b2X>`2%I8(Gcb8dqyEEkjaa-P4PZFbFwp;3U0?DT`xh9Vgm8Hdkz?qa1s9NIQE zcF18M!1-7p^eYk&OJHc_6d8rZXi!5s$%2Vl%funj-(WnNk&&@`|F3Dtaq9F|l_!T| z8GTB>W5}XuCL)P4BQyKp{zMu6JRsxsGOb!bU$Uxg$*Q}rw^OO2i-`B~!7H2lk?Tx7 z@96#ou*&TFdr}%tX7>DG=*~PnbFKgVP&A0CU=eTb@-bo$XriWPV&)#MSOg-!aZD{e zQ-tucgQe9mfdJ*h=W%5jSy`u}bM8GVBU|GfySganROvKER;4TjWS_X;{A?Ei?=G1Lns!tE;UMw>+@Ao1@32a+V&dvb8n$?A0(|u7Gd2^4Mwj+|({R z!|?N#1aWY_VrXfYyF9|8xpuhmPdS<`3*Yk*2Y96# zt^mj**vsQP&a#3Z~gFn$1m@ON;%*KqI4_8-e|8*N(jc{ftjz~x5rE&87Nu74ut4RViC zew}>{KXtubs8qSCBpcice4VuV=OBIUAUQ5xu6Kd0rncTRXRIhZOzDB;Qv*KX)K{C0 z&9u3IA*7jOGiu8OW>m

CI|_xl{Z41$*Xg>$@qlIOUw6l7ad0g1kUh#7#URm5_Q+ z^6V!fMjzUrW46?;lGH(ppe_e3@^9=ci|KN3c;anhIdn?t3(%{Q>6LI>TqRs=D6Fb5 z+^g8;W-g4%Mc%(plggPGxc{z@)b;XTJl8CDQYT^NJT(meSLJ-AXXYfcwf`@Q62;1n zE7!AG+WMohz4+@nMSRu2UQ8FptQ3irrvV9rg3G$)XK)kO{;+d(^OoaGdO>w9ZOUEC z42+b3z$&qRw)K!l3)h>l{-t%>+4_GSjR^WAN6n6W?tAF=Z2*E|SOviY#RL6Ovuo#_ zAVKq(s=GMlb=TkGJw744S6z4PfMtT8e%W*#6EgIBbN7NY#@-ciS2#U+MstTZqlDxV ziD#7`;)HLoljG#I2dUEoEO|H_tQu#+Zxa;LC8j)_7f{CWQifQVipq;rv2&UxS&7280L^Zm(ue2g|NWDnt_qgUY;xPPmiGhos>_A)Dh-&Iwl_zj<`2e zXp5Y$)u%ZD2ao`{#7gHc}Gv;VSs9a7_b-+Qln+MY61l9Xi)1~(E%q%R$qqJvT z#Wu%*KPUM|^-IFgYLbyIgZWdUtKCxq|I2axjmHWuIauV_di00py=5%ZPO-8WHXKhg z*Uh08jA9U-y5kt_1Rtx{ZE7qnF^zr1AbR)xEL3R&jS_J z;Od1?X~rnbjx|N?{|gqBL}N!R`Iz4ESLf;vC3v3~n$WK;Iq&J3dUMM;^lkO$>@2gV zDv5rGZ|Y5vmZTOG^Xs7nH&mkhU8N%CzG$OkYJY~RxnD*K6%g;N2|ED=P=;IWd)Km~PTF!;YU%kQ8>lF<|3b0c~|F9X% z@OM=n5)us!4Ff~NU0#rnvz6V!@Ry>WuSHnzpCfqo2gZbNtDGO}-w>Q{p3$}sN8SUs z>n<~{WT;oVgNtuOxSBc+97bNJ6J|s{HQ2JVtiCpjt>!pF8dNL{gmt^|4pDPZmqRfG zHUf6s4m^BFM{!lk=SW;9ykV))fNQ!py%^&U(@FgWgC%6qi1iSG1F5{)t zqplUU8B|G3Y0wMkKXqHW?UtG_rHf%qLJ;hXDHi>j0F3I1QC+GXB!IHl+Rcae8MgMX zfeni(#~%^I42Q`>LmyxpH0U%e<|7m6Wkc89ii62lD`hcOGY#aQ)(n)!2xde8B33#F$umKOig*V+T15mK~Fn445b**&r!l>g6Q$~j%WO;8fj=^s^}XU z-x3PpV)TDIR@4*wb81cXOtV|US3aUmS3B&;sftS1VVqro-dEje{?ljr#q}~)bYD*y zQTW&~TncrKxt2W+s!iNf3ZRjoHtVRCXDFxb;$?987cH*Y!hqlSY%NrPlCxP%ryo!Y z>9+1AHPh*dZY{&ITODUD>qpv46V2~W^gW#p>n$<0Yyl2RiR++Ns)Pq%U*7UH&QzrJ zl3-P28sT)BN(&k?o=VGCgvsce!fFjT!I`#IhDy)rPAX;=O=pu3b3<-Ug$}xtpz;luZcY>wHj!`i=s2(zC6VcVBr=g0@HWZ(k0Mlb9*mM0uCIL%t( z(h~{4kcTWnk*AMmgz*Jp0%|sa30*NFHMlH9xHSq?7PGSCRJ&c?J|51=0>#%22Dr-$ zu2ipHn?piH#s5CREAV^&3DZCOyPnf4srYD1*>cPga*`dhK^EJ~Aoy+jl$ z3c}F3Zsl?~3HU%1L@uORx_IdbAyGmPyvI^pX|RGY&pvEXl$J-5kd-6Dz`#Ju!lwpH zCO{sMwb>dKQ4K1NP=p|nhZ$(JO6#}A|4X{JMujM71uMh%S(#h6a(1&5jMQ@s91gs< zgbW4&?gu}mfXTcYQeYz9InR);@-9&LDqjtUG})4W27-aF2R!1@e?7-u~ZeN6noC$#o&Sc5aT4hfd#}`HU05Hr!Ww%LZmK zM57>{;-;>q$tf8XSykMU?TKkFb9`L#&vKolfxe#J(R=8fg5rkXB!Wl@zTLcj(Q!?x zl_v_zZ|*ktNJfE2D$a|aeeWfna<$pJF|8Lw3t(QfW&Jpf)iF(wnz1<1>+3o8(D z)irnL!PH{=QX^r7hvs>VnVB$7#dJ$C@RV7)>+a*TPM%*K z?4)hbfU{%D=gPA}qUNGk?@iM9*3J7h$W z+Df)|#@(_Q>z+^sm~p8-2hXipkX71I^GX~G2j(m{4&`mf!V5c3JE zO~rodP`FlBCE8F(N@^9?p+xUYji%3?Zu2nav@1CDl*M@m$x!0oEMp zxtj^Vv6EjxxlLI%j{dCo9I<5as)q>gR8k4A~S@4>b`l@Q)zU4UQv)0L&u`i`(t~vc5%DO&+?Mrk2X4uLqMrE1SPlQJrE@NVA z8`K>2aGviORZSj^K|)cAWmJdob^E|Z5G8|vinBwXU~+@)w|`}@SD!@F1_#w^0uFwm zN%YrdZbzdBiAXU#(NnI)=7v>3j|i_~Ky-OpC4w0zsRSXHIiD`UxFitQ%^zb46XPmE z%8FvEXJl;6{yjj@dzejEnov*vC7$OH^~xoYmIXRB}17psp+I7G7ZNH68;M(O^ocMfQ_|tdASQED05F-y?q=E35X#N_>?DDym+MIjU)TeC}>%m zYbU3oD^Lq5A**IPlv!(UWVxm1+dVT3xw*6*{P?RC(y-nnrwr*2nAq`~7U(6vpizDC z7ydgv7w(BYKAGQL0%xBx0Et@Vv(r+tS%}b;l<*szBym+?PM?toKHgCT=2cW8kRAXK z3WNvO8bq{QO+)bb+VHcpi$$&o(W;Rw5s`%RL?Eg?=h9$7dPGg^LZ4PJ+^fQNv=vf* zEl3lhV#GPfDH?k_<|CKF#kSU%Wr2LDdiwbA!sxFdYB!%H;PC#DE8rb6b2yq&wS;|; zrLid-@n40J%$8^v49j&_& zDl?nH{seRRE-QmDV{qD8F9bmf=T&I!M72MqZ*uk_bYvOOaE6+-)!8#L&uRC9h$te#Xx{?f zLcXxis?NR&!G3k?nR}Lqx&y!sZ6ldvm^_ifiKIp*?lgh>n{q7npCt?J{2B^($Rhlv z=HHyMhtP`k8^O(iKM~b<7T}%Ex=vJAdiMT3Sr;BMKx}^ii*&PQd9U`o`JD@|Ov%T6 z+ApCQ4-(fJO#HRbDTTHLy3afME3tG<=~K!>pqffU(Rcq*5>dTZZ$>1Fsj1CM+0V}V z8W|vW+#9;DOPCtUT8^EspK6>nC@SqG_d$YC!1^Go8(CcL6%!8NkxS>93KqPZFJeeG z5toM{gR}>*#T)dYZ}ECXy@uUr)}}bjODq@C{5vEsO~i0h6Vm(1G1Jq)FT-9Iuk?aY zsWK%59fDHw?>9IplP6(CAxrt2mG2~)Zt_&Cj%(r05u+K7pLo*? zzXo2qnl!X`Tq5bb%JVyFl#cK_X%eftt9CpB86RnNHB%eX>5X`KH6;-MO0ulz=dr>g zDoj)AZzG9#_8yJdxN2UpRl1W_yT^yIR=V{~)E1D)aQ!kQ2|~y)k^w~Sv4=v&;6rL? zG~qDB4Ba!Wj~{0LdD8?jKu}Fe6ez(2M1FkIu5| zI9Yc>02YMrQtEs{pSl42+l{#Uu|Ig9`!@KiJ5~4Auv1=#=HSON2b2}z{I@eeucJYe zo;)5;H3d4``pkBZaYxN|SJ@P*Jb2gqAU(GO-Bz7+q+TaLJ48@>AThkIXU~0a?0XIw z{`<2In)P<77VyWRL|d&o@Rx+VEfdOtQo^u+sQQ)=j`oOgU|EnPpdM1?Qu|rZ@{BS@ zSSyM~3N3w356NS_QhSo)qxthp8}~-(t))j57LC>g9lq+eCJPPp2aF3zMlGBbdtib9 z^GQ{@jI_?BJ9``wl|L?0tx|0lp%996PvW4&BJ3c7+M zUT|D@i(@xl`=@kKO3iXZMY(e-OJ8>1(*hBWb9S3eFw zZ}hP*v8`#s=D#LIz#vxDaYF)ql$*Y7S?P|F_dQhx7O|eSt-6sQ!9Fhw82kRR&C(rY z>nz=~h@->gVrKpgay2>@4{+tW;5BW$-ee>PS0$%n84r!6^BwwOc-+#(U3+%NMq)wc z`WO~@BauLLj5lfnuY1wfx@;m}G-E+$5yA}ABjT$gS)2v=B_w4=S9tZiXpq8^EO%6; zf5Ot9@Jwb1N2QY~R6lTfx*20c8?Y%=!?V5I0X0*J4MaO54VxQXXa9LqnpaRFh>RN| zB~qN^%NB76BXg!#MKjN*n>Ba;9%AbC36k#SpbchtATp>;bSPXjwv2CR)!HpE@9Eog z-<}5g>s>)vfDNx8(i4)e4HZK`+U0sOzMBK04Z|*!)fb9aX%;C(~k%lkBR|G z^~lnBzpRO&Z{QPbyQJDZ(M!S8Q@vmt1~7ZPupr}eBPomo{~H1cK}Ch&Tn0aBceUH@ zj(amJ#fh?lE>NHmoQU_zN1mx?K6WrMj!@BtJzLd`(40_;ZBY&hNEb!qm}%%#E2w*e zCQDloh_f%ID?!{0mf?j|s?Xp~{1u;i^>ZkI4tm(qPQW*eaFuhAf6;rD+m&=yk7ppo zZziTFNhJ|wkBP!g22F(B2q*uJBXoS+Ma+&R){Q4R420h}V^)cTnj5=@XioqjuhYel z9~T?jL-7eoN=n+b|KrL4Fq6Xtz=nsc)5!g^a^^^^f1o=LBC~8>6JEJz|fza1lgx=M2Btk6q zYVKeG7xG=#x;#5Ov#6<1t77CKdFLdYOF`XWMXN6_$m3mZ?mIZt*$f=(K8{3$Xa;{I z3xW$BqzY^TRenx=wNRZ~2@*U)&JaGtJdv7lC#z2=R>Z)shTGy%EI^TZUCyuBpHiCn znf*gScNTi_y@TUEwSEZmhi#Qs>jbaE?)%5ZXwaWjt67px?g5@XV$f~nGa+gRhWi?S zFRnwh|0sPJouXQM4iNz%@~A;VsAiCO5`O}kf7PQ(b5z;J#=?8tPVj6u8wY7A8HzVa zQ4KnYRo7-^y?HtvSM}zrbT#4@LH+THI#5&A7_?v!t8vX}Yf2kp}r>qLYh;*$L;^aZ`+3ycsY@8WI1;9+Poc$Bz2&BBrXYi!`qIT}(nn3rY^I1Z}UN2)UeJ zy#a<%lw6i1iBTlQJ4aOea!T==NNQq2E3Ia-y6aFQX*Uwy+F3>6aN4~$f#`ZUbJ^1> z+ZoarVeO<~GO4eZ^NeoUDse<^#AIyg#aM;%Xsq#^$L@Jjb2iT}SDumZa3a6ofSdA1 z%!h~guM%pQlKiS4Nr!R`EtUxxP~KXqPW7TNTkvG1e|3=ql!?Id+Hgq6Ai%W;Bth+X zyTStl{53Siw9Bc+RnQmUg=4FqG*7W-T-mHEijue^Q7chtsV11!3P75r*aNiwQze(k zA>|aPxJstix9>V|0E>zy*HlfN-4!>IKZvcfWoT&a8S12ryD5ng0IbzFd8Mx8_+);> z{3aCeU^FiBhE9Z>IjTllJ18X*nwQ_*Jxp%K7em8Ff;_Jwuhx0NJv$qA-F~^;?9>;u z-LI`0Yh4D;v;uW7q_WC0+1$d&;B<_Pcd4yihZq;l($Y~IE#{e7;-yR{km(y&wlg+G zPcfX-2{iZ5WZG`&)x>l$jw7RP%p4tH)%}^-x=Hj$oD5C=PDn`jU2xJW;&2($sajvJ3b7p$HFvlpElN{$m^Fn6ahOu+P6^pX1NseIvow)j0X5 z6Y*V2AcQHtq9L_Vpi2;w()-&F=C@OrI2*TrRPdOBteAA}yQ`|4@YNGM$&N$Z=ouv# z&^;XtTut-)7s|xE;^@4z#I4gbpM|y(q@vwuN}Jfp_gvvj>)Fa7LlCAVR;_~Wa*-FO zn|8gQZb4e9_bU$jO*4=rkuX*WA$Nk?&H{BPbtrz9j8YgBWm2?q?$hVmzc}GNC}2Db zQLnf6IM6Vj3;1QX+WeCvtYqk;C4)kMNG(V7dWNS+g_b^u27B>vb{azjZI}6(hRj-0 zey%`+rikoSUXcXpoT7mcEe&E7H?FvMv9rt0E5CWTv$L-et;v<1NA_su|24<`@BZpL zmNZQ$(`LS~|8$lW7M$ccSDi$=gD3|fL02K=N|!*C4?C}cEM7x}l_ewn|)cOT)lq;5l9mCNT2%01u4>(>S!?!Y|Qg;t%?0;cV%8p8n*N z4%Qb13eH(bzK1EauKga)9Tz*8-d=Z2wbJ0Q@iR69tsx4~9zRmk^}|u^|(ExR$_v{ZfUrH59~&D;hVB^SmMD z6(kMg0X*9)5`>ZAOO7 zt{*3swpp(*q^6~Dd)-rg zOOvIjfyen$dr2eD&s)C5PTXi!O1`YZgC#7_@R&=~W+JiFDI{u@iiwlJe6XZ>ddRU$ z)x>%!@@nMDbS9hQczb=`tVdzNtbnVXw9 z|Lku!JrKSQ97Uhy|Bvr(iWLa657w}LiO73~0TQEsDWb3ZD<5%`Y5I3?JYN)I_?!s` z@=*nS?1=8XpS!N8wBs^@pgalg|HYN3eN5^ESU*|N|6^vrz1fIwt$DeY;J7X^rh-t5 zNGDIn+c-5@H&Q`9_sApr!1UDA!>43p7CZaL$x3zn(rn~HIk8nn8WS%_8V@y?OcJiR zA;dC_?hjI5|hw_e;p`2mG3yRqKv_d7Oq!^NcMGmM(ZQGQ-8TEg{ z=|6*B`8{>kEk61Kfd(2{T6@4XyTa}HLg(-JYMHCul&$SBrgSq)9q1Bo@`*i(8f^iN zpArP9MHfH?_Z6X9)xN(r#qp}jZ8qsD75Kppr|#BM|mBh{DR607h! zv84Wx7zrDp`is3BIh?()O%jkfGTQsf5HKlw%57>j07W65kgpj?qyc4`bN2xI3S@P@0B5Qwj({^|>58{`_@I>Mf|dLWM* zUt7byySqEj$uc(b$+ zP2#zSE?$|0`0Y91;Y-wF3a7;A(G>e!W#dr4S2p!p9qJ_TaqaC_HNOLKKX7aM-j}T2abQ2Z7i~^|PS2G{ zMFv^4t$rW`{`toHNW<%vPM#i}>O$&7ZChX^p-w8!eMd^o!CI9iflADHIy5!Sj(x(p zr)U7tR6m6|q@9$G&T`lv!*bNYibw*XnwyZZ8U46p0KCU2mX_Et6U_udX`RIZPsC8# zbLwLGy&0lCZu=H@H?whX8S+oR3BL_C3M9&a&yUc{4#d7xs+fRZAQO}*Bgy)D?bh5; zbDwz)%pt)6L~+>Y%>HRS_hF^BSTFtjvHxg7Xv8{@sgv{UlZogrV6%h8;_EX2e2qQ* z!%MDP4NapuD&)2Dz{@!@x@MdZ)t<#W2H2n1k59N_nH*=GcBjz1xAwgKq2Wb7&r7P& zGz2Vly5+&Qctrb<{Vst(v`>$-Ux+ve6VKDzpfv2&HQLL8WnhvDXj8G0Bcy`tH-Cp@ zxMgTor+Cm2-9|v1o71__Ep+E55ZaqU8^&=?Cmi4PztOX$wZF|Q^;aNx?a!35Baps~&Hc|TW-)X7QU4Rl6;P_ds( zKF#);=l5KyQ=nIKA{a0U-r8)iN z`}O2x^NH>`2Z)mYIgr%>H)I$(iD#`+MS7l_y}LXZe)8ZoH9JdMasX6)zNQ`xOioT_ z>-eO4BVL$NSjJ=Vqn@zS$av1+m^019pMqSF7#noa+YphjX~J5kN6>-`C29&=v3V*` zmPg+lFW{JIM@?~3GUD$k)_1V235}eZ&P)1N|$Rm=$g$!~AHDdbk7NHGM$n|;SiLtzF-N@Lj1ji((xU;k` z;pjIXP4|`&X^MCqmg8IM7H~BX&wC%Vg*IkpXYb1VU(0~mRiqMh&D5LkCs*#|i+|5M zbu5Fsrbg>3s19)Fzv&~Uw;*Ry%AYu;V{*%ip`&Vk{xGwjh*Z{uvYjlcJw(k13q}MN zJzt;hVTUVNAX6NvaUI`n2#5*}>c$9l40G-dg3~Q;CvcAm-C$QWWrM9p1U3YyhUF*{ zQN(DAR1%qi4PtnsUk%1)R>vyM!zqs-z5yIyu&nt^++aybxEJ=z3alUvD%kK1$@p3R ziT`{#Z&r=tfOF{I!_PbY!DK10w0M*FzDMxp;&5`yq*{|dM({xJWJ5RZ1EeV-Ecx5@ zNdEoeniJtFjsi}2x1z8}fF^bsEK2lrSe{@rj`Lx2B?mQwe%g?ZR%}p~fSRblc2ga5 zhgUCgF9cTL%7`)PrL2xQ=3}xDKv+OoURnUch69NmVga40%~SKU_?{qg!3caMA8vC; z?mwT_p-)!TYZ@tGB-3*HAtE~*vpW~|zn(kU{Qz%2r+%~oT@OFsG`D~jPF8;Y=|%aU z(?I2gz9Q(y>PqfTi?-c?L3fZVE;;%#=0S&S>v56vX#W5%%od1l{SuUAxk4W7~d5ASyBGv{?6jS#r|J?J>!3|QJ_tUUn>$ME{igdOo9 zSy@>Jo-;`AeMnu!P~FR3zeQ1mdVT69Sb0_kjSS)p{Db>&Se_o- zheGR({~iQ2Y5R#3|MEg8mNHABmZVl!VS@)rUH*1rwdsZd)GMVDv0fRNO`cBMap&v? zn#)Pb$a0N~5hFn% zZ``)~`dGHn0p4oN!}~WNC!pK`@mjAmo5_`YQZLKC|8lpLDwIRXr|20%PPhtjAvNF; zReOy7UA901F>BECYEIS7BlM*Ef0Ori-Db?OYMs7-AZlJfg^kdho$f(9srgH-yYE#i z`keofxnGz#p>Hyh|MTEyIygv;9*~^A8P~XqR-HXNFQqh1ADVhBVNjA6H`4ykt#;o_ z85l%#{SVaE5mkozv&l3Y^_T@^E|zLVII|#0z|kT+NZdOXxG5H4^g9*h<3dw__N(lb z^Yj34Va37k5Szb2LM@1;bFH3hE?W`-fju9y?^A2&Yy@|TLKQJ) z>3KHh_pHD><@<*J)y>`2>wg8X&%lvew9DQvzi)%*weZh-VaBS9o{o-*pc7|fl%jHA zaSB;dbP0Ye+ROm<&)q8OY?njHYu-J82w8I2Wg&k)om@gJo<>BJ9rwfawUhIubGGho z+1oF^j3ak61G6ZF-7d2SVu`(u3Au{6<8x_xPI!cpB0V#+@Sjc3!G03i?w9e^bTA#z zH9R}3dHD!Dl2%?!Xc3$qM@ zWS#rL<_~YY$ZQNrYqPOA5);E&PfoYKMmMfiNtR+YS-Ej{sD0^{`m(?A)HuZ2+Zu=9{N7zY~OBrbrZ~RS2y2#b@#TEf7yDzwZOj4%pd|WBSdxHn`aV>$qx^% z-!M8L^TQKpVv0z1y0vfJ_TzQFoM4~xq$^3whfXE@(G(rjINAm-bt6B$gE2kD5#oCkUkR%j9W5!b%l*ig+@A= zQxH$&Ni64i@1kp+2HD2<;HdrtouA%ME|%NRx8(G0<2jey`0c_aw4tZ=hHd^Nv{QwO zHQZk?t*bbt>(Gw3f9l7V5$D)H5i;;s<}j(*Z(j$H3}5AmBo}5@Dn-ueLSC1CRR|50 zT3tQf(I`Ixd@`4ilqud z3_2^zeFZQ0RFh6VNA-c9Ue{;Lp9Kd0&$+bmRyc;sk#~1a*Yt6+(KTHfu&>#F>V@InNpcXSN$mv$+k8qV=j`Q`s-ohx)xzSt=^!i=UD0wbHe5JB+;kDiEG<7Lp|v| z&DI@jgHppp25N8cr%1M3JqQ>L*a;m4X8+Y%M?Y1=<2O50r^ZJ4wyA&zwW51pg|iGd z*oheOMb$(erUFiESE;ie{4RuXvq1^rXu#rZ2epo{Q+*Lz#HU1YyLR^V-|)?&5*tzZ z8mzIaZ59}ePGMUn=c+|Q=!mksyMoIAQemCf6JW8!bQGA0yhtty8HfI_d!uc{d3*Z3 zSMDB*cs)@-QiH?k-wOK(!LNaT&hYE(iw%2sX^hu40(FBEKeW2XrH zB+*{o$()6(O3O;qb3aE6W%WN!)wx|y(W|Pe=olEBfQS(%ph*=dw31H%a03$qoR*ol zt-#2+Pf6F4&jBgnuKeeL{5KQtdg~78j6QomkpCq6{+&ovrq`k?&4Rd5F+uFKA$#SgbAp)~vSXZbkr5O=x7azog`O6-{v{$j z;V~`_dCs3!s2d)`TTJ#Pch3^?k)4Iz02$74T{A}HBdb&QV4_YjO2#L(4}We^nb|33>LHQFnpI10O2 zhmuuK0`5vz(QH7TIK+Icq984(3+3~8Qb4NaKvR(s3B(Z*w6Pt)X`6dYpW}{p=*lq= zz=W5$dsx}9GUHs8XP5kmO+YxCOK6C$F0mhYt99J?Dv2u!B|62^>s~c;<33uGi+sqf z1BUp1Ed8ButcP0{7HmF4Nh#C5u+1AWX}s~ZFba(zYjk8(i2(>JY20gp`Jepf9leCJ zKR6_yTuXnuU_#cchSg2v!t7AZp<&8Ls; z*x^KzYDnJqMK3dA6CET z-+MAY0oy-sxIaJCd&acwyF#7i`Nx5`a>+!ZkaU?;XJ==RD@|QpQ)}<2I=%;0OnWQi zdY|T>I|$N?&>-!svdr70Q59w^aU~=Pk*PBl!<*yn1{0e*ZRsxYGy^K!mgW&F+Ye$h zI#i01Q8}B7KCcP3du@X*2i#7Zn~au1X{YP@rqUJP|H0MuAq#c?JLM*?!XX6hcQ5Xr zT$bmDOMTc%R`X%sk^l4ac^~MKfv_kRf#B7J@Qy-Eev#ySA z(mGl%%7Odf=#_=;-KNk)Q6+;@Ea%v0?g*&+?{8h-#Ax*EjSIJ(!1Ye^0EeYLTJ&7V znSs%8d6nmSGfG{r87#dpHJ)_gV`gkX0ryq?mV+nPjy!|jQSA$Zy>9LyDb@;+yA96s zV3Rh!h4~Z(wX*VjYgdrdjN{9#>DmPcFE1}VB-m-k(S^M~@1|Ya8^ITezSpOpo%Z={ z-wVBoG;(V`9mcQ%vZ~#H5n@Oi)9^&YU7oANe*C_WeK6o_)dV2*veSm6VA?Eh^xk2i zgG+cl{(u!!4Y*cPa#K1LDXJD;ijBU;rm%jK+0}x@f?)K==?^8*0qge4I9O%ZS$*K^ z;NBU!4=D5Izs%0-_nLk288b>`A}o^;$EjdR8nDYAXSxhRQc6 zs>Qjdla?IPL1ji0$FH!qJH^emr_;@%D8F?OaIa3;#q8pPPGnH`G{KQ5Uh^oVDaH@; zFhseM zZ`&kP2d+;CKXT5J2HsYl@{0~ad<{ogQKWYUhrMx7!m9SlE%>iOt0Ir+Ni^OQL~rPm z8o9h(UBlY;VM9M+o*w5AvyE2UpEK+S{FeTsI5&e)=r9hCWk6?axq}2)0uDebL;#h@ z$y>%-6{m7C&4Teq*C~Osl9DNk+N&f*7kYgmUzYEndKCzxwRA@UwBFN2NR~MUmbmbx z@1sDEOYLmh=MLBu)ij>|2hBv|!QTqyZ@o8}WE&vY+r#E!a>L6h^@Yu}o372eR$ATK z$vP{QgH5l{u*RBLD6#0T2@iQxr$hyHl>k8;ZOa@5Wo=pJR3La)QKw7t;ELXNfsJxM z$;Q^!v7vY3^s*ui+K)Em{SmGs{s{X`({vtOCM>Wv5zz>RsnFZ((DEj;^1 zTF5zMILa7gmg=-QfL!r^b0UCOYG4EjB!Cbot_6ITe{Qz!z7rj--50iuBxnb!{o%Q7 zag4S1^~I}QeP%E{)yR5EyU2efsIOZ@&cM~vEPm|DEYRa9nm+q!t4ov!{R(#` z-tJ&f&;qI3*vhP0=f2?bMtj&OziL5{6W>>6E16M_J-9%1lo zPz5?~6*^jgnqA>Yl?}Th0?Z;|YGgS4QFU}~g-9_qfNkU|O+(7auxe^zHSjabpU#=Zx_2cjREbs4y2dss*&_&cf{1d1)%JH$>L_)}sB`HRu@ zETh4BH%phv$6_-6i6VHjo$j=+p)Cu5QE8kCH-vcST#O!ujc zah0mw9nf-fEFH{TFKwnfV{v-80BuFMuR>1cH5qA{JW4|?LM^C?158WP*UkMB{bL$l zru|_F>5ci6q%AGZEwt0M?j9%YEkBrp3pU4BPvXWps0%BpN(x&LBPdrQFE&xJ%2Wn} zWJbBn%Z&r0$W(g=M3_@5;w2$H0~c^l23v%sLdTp7sl*+pnm9i%fa~+Bd3U(FuQ38rS4QkK1kg>LzC=UG_x70EL@uS?Cvp0D^s&Jn=V3K zc-l9KkTZAGYGj`hox&E8uX%T9HT5oRhL@q%5$j2Q_`9CWiE#}>yUinE z*7~^eC^8}yO2BMPCh*S_x=VKUQG!F_RsKVF6j1tU&c4NUt4UOOd@A<#P2^T@R9bD} zycz#4N>QZ+|BzY_!vY=ebX*`?OtegR-Mp+#fpH4&sEBQVQBDgvE6>~U{H*`%Qvzv6 z#BA{Hr|t(&0b*?(ou|dVSLnqyW;TO9O(dh>DD8{bf6GfMIp57w#7H&{>4v+o;~_ov!;17nP=nJ| zkdt%W;sn7Lsh`zE0892KTR69H1`kgR4J)>rFQ*#a4W1TCTKId~gc^F`2o#Hz_mX+x z_Nqy(6iECGAYVqbQM3xUeZ)qtLMGVaPAQ{wI&$l4{Pf~-A;YRkRu^b53J=gFvK)ff-oZ4+XC(Y_fVSn>Tw0Omkz6hCPneewHBQq8ZkhKUJ`enEVW<6tjH}@|Vi|NsC(+UUW+kB4jfkqg}jKj7W zwq`N*KCnwCfJ^7XBb@Rz_!fG#bNXjN!PljP!Mgyqb!kKyE2arW#HI$|?;DDusnfGQ zYY4weC6^k>U1gOK-%D`ioW$QP!9NIh`$XJm|GHWd#lLG@D#l<_oE2yh(4|xu`F$

l6-TAwpnSd#v>+Np2+eJq!we2Ak$dzinUbR%33WQ z)e4uHCe8|1(J8{Krg#XD?1#8xO)?E}>SX;BS`DS52$g!QzP=+tL4hU>k1z3qP3ygWeN{`eGp92FcLMdBY_NcT92nYdF%q0BO~Xb@r- z=tcgS{$d@wP@#f9@R3a>foS*gCg*Ca?|`wTv2hSGoBFTsxvFpS9~{CRf?vDpf*7G+ zC+l7jAb4La6_F>Zf7^bOyPgbI6KlvpfgGCj?N|o#Z_+ziI~Onu0T;g80EW2JqKxh6 z<88VcJ<|%W5)iF`bKE&!p9dzv&zwjlror-TIna9`ubOCG@k8R0 zTVGb&(|+{}t$g4bM{~Z~?JLd~o=LeddX(sFZreklsRq+pJre|&QP`dam~KqEPZi;s z2>SYSO4LToet}JUF-l{T{gjLMoTQ2_ydwLyE)($X^aeeLmdsSY4-JETRYU4+ZAcy9fFE%my4PNl;t*O zV-`^8Y2uX{+$VGnU0OJNqNCy!k2D9xZ8nNm;rW60mKrkbq>ddtz4a@}w(wuf(0=E5 zY5DZ@?V2k)?1^QV@E{}8idpMeLL1C<{cI>bah)+HD`xc)y1W7tt-I6g^UgtjF+0QW zk$x<=*rQRV1fQx?xg~^hW;A$Z2Bh(EBIUpKg*D!(`rd)QDV7XJkTW1pu1u^6o1S2# z&=te8NoCP1Qd~2~L`!-cJnn>-k)_n<_D|Vx%Ry(z#QG5NPh`~zO_|8Y8 z6{d@*PHo2vxe3CG5#qv`sKXJ3I*F7sm%YHeEx0?6xplbsnV|_)S?te)cwu_wtUW%Z zkcM<)z!Ktm?bf~DLVxesGkRp)<0u&h(TsBuOw;Y8n9v4wVJs38F0m!p5x1cftOzng zu{a7#S>*0h4tQ|_B=V?a|Lc*UfaIi6z&8L*dFMPk*wt9%)ftmjl3y)0K?f^9;o7T5 zU1&M=6yGw0Cje^5J zu3PQ$j}LzjvXe3Zg*YEYToL8jqx#RWN-Q5dh121-d@_$}D@*-nmpS>NW{;jN+`3Euf3n#D{y zR}I5mHYuR@p{ti|uxltCqf)&z2`}#e#~QZ~fsBh{k`^GYVQ;XxbSeIN6OX;dZ`Dvz zLC(JfyfD|O$MKE)`7^cvT<|`<+~|ggl}_ z&5jNe3tD+3zLHE**kM0eBC&$Xs)Vjg{DEGny-kUxf>NyR8)Af+38DVlRiCJzXkHv+ z_X)C#rL}dW3dAgu+m>*$(2y!lCGj=+IzP>9=43N=9c-W|L$EJ28nC9%jyXa{JCFf9 z+f`Es_{L!&oD#b&nB6+4G+yrHZTO$oK=5nXGgUTn6a$VJ4Lsxg+l21U7T-@z_9_i4 zFT+uU&CLp4KDxM1xBZEq|1l4h-vb{ueLg}Wq5j3q+b6kC2ra7e7psC2NZTWMw*yf_ zo{r0|)JEjh&=B$Qa<@3K8A~m==aqO3E$vAc7NtSPe><+9xG%)VydyScJ(l;dl9Sqp zLM^!}nbx&{+W4+b=Yxw?%o6*nl|n-c76^$I+0*V_;3X221se4h^3j;l1Wg`5d4D%J zcJQL1Xw*Y@EG485;MPN1)EP$QbI8g=e^~xh%IQ*(4dappm!u{bSNs=M{d&CVBVAW71x`%G3>qH{aQU{#!2J&t+)b_Tue!__F74;3E7X;7d!fhB3c@Q z$N715`;-E^^h#IFCi^1w=y??`ZNOYcU4M48-_5M2uuqS?n$p?Mx|AXnN3!xds7}}c zYRI%oJmZVJN@E`7EWTl}TLntaXE1#fl={*zb`IlodMz38XWPq|Zd$^)X(%tRg}Lae zR&$+h)mmS_AHsNsSl#%J#aH^@FyKz{9aE#HU>e;6Ve0L|m+Ud(su?8K<4Y>vD@WGz zw}-J4KVq%N=_3}y9IF$qOyy++D(|by2_s@%6WZwrM za~H7gz~1knZ6jF1Q}O%d{97Vxp*S9WX~uNi!essHiJx=+PcS9vJ6oOsipCRn@UfO? z|1B=7`?}Zi?Ml<^O_WJ*M#wWj`{I)X?zXp>&c819G!UrNz4lS?>qE!?mL%V}s>6-n z`tJ?TwQ~;Z3%Z&i8BBAkBS+n;0j83OaRUd2x%#OgXGuinteY4r4O(&%l`@awO1;1R zSgdn(M=POmZJI|a$Sk+4IRVjcLf(0hVb;(4)Kmcdhwc!U?kZc%W2CNA5TAS%fg?KT zsbp2~M<9ApHyH+kmC)phu-=j`4KM$2CeaL6t&R?kFDuLfUoO^)tXY~QUwwzgI(&Xo z`V`o^W932etG>UrdHVrO3DWygGRC9q)st4@ef|uaqHs}bz9VDdMhaFseqJM2Uw_%` z&ku(oF^I#|_cmvH+Q?_qYpJiU#^nY!97E+$Lr6|e|MjS%*yD6SYk|h25CcY%`9RZaK0STA>i-aAfAikh?n5~BjRQF)r#o#|p8(Yf0r@i+IV{?5HObEXkH(MG2G( zE#K&oDG}2US8U7ZFBawN=TKl5W0VyYs?d7p!`*GkRe{@_Fq^XMKc%|^Tc191V$|ZU zcX8M_-J~SQ(c^E`Gihp29d?b)^jPilSD5vBHf(aD%TpSSz9@fdBl}Zttl_0A_eZ*x zn@7k^KHTdks=J7lA!kDb)JEd%ZcD$6*n`39+yo&m$uiu4^{VeAo(lV2%KKOS**oo+ zX0yFnX&gw-m9LV>&WuiJ08-`ee|y-Tg}{Gis*3IYUEr2YLWCh36uOuD^L{XR@gY!Tda+03O-kT=-TkJUe&Uyb<121WTV*Sl#z|6d*Wt7-~~2 z?6XaTt{_H5mNxKLWz98ja_zb$FMBiJ!ec54Y*O0cw-(0kzgO99ATLa zbt0prVtH2}z?4DTmNhe$%ZWG`EOyQ(TJBft65X;RY0zXKnEyx57>}n>i(a*XENbWk$vR6Gz{wb|b)2!jgfc;>Bh(vOK62F7BI7?{cvm%x8%=-D z)nUS^IW&6<2@hj~ROB2?Gp4_$8FUl1SFMko%p1Nb+nI`f_>FX<=#!lYbpcg_r>lDW z$vXthbQA1O%;yw$96~Li%4Xc2wGsimEXPq!)|*$Q&gE?W=uvO`~PR1MfR zK}=L(^88;q{U=gdG@k`k=dg?Bqjz0>m#nWy5uQULcwrrvBNq@Q5u#9Jt1+8P%!l;l zDUWlK61%k`%;Q4`zw}u-gb5e474S`bp)_pRl`19R$e8=3$ye>D5K(JD&16DN*{Ma@Sdzt_-ch5!5G?*bx$w?B(r6Pz*z=@SgaL$1U$i*mi?PZvJ%$;+A zx<-mC)kqO=@AF{ysjQ8~Dk%&83fDB5jD0@YC>iKHZZbkCS*jsuYr-52S}96?1ekSR zr6gW*T4P^QP$FFUcNlxaHMXSyS$WJ-4E*5PodsI@LtR0nzcV~4MnY2o+b_ltC$2A~ z^U(emWDv8TSyDnx67uR6yaOBj?d|0uWC+~tVH#SYV3Yhg9S~mT{emDI5f=UD$aCe|BN0DPvd*o116B+k)b;n7#^wD z!+%b*;jR<&P1gVhR_iMMav{n|E4bP1ZRRs4A20mx{LI0j)`mn@@@h1)C^o8Hc4GZ5>;5rBEVu1 zebd`k*E+>EEDJT>%#^6B#DeRl8k`Q6PKP}Bjc`Eo-+b3478xUMYow|Ivl`B$E)ZpWka@TdT95Rm?$bJgcZj1Z>EWo0^hLH~v zIwK31yDBFE%yMX`Paoqhvl%}(Dh8vHi{4JxKE3J7v}*;cQJRuOfd?-}>wY#xabbvI zXx2V`u8mD&vtMSHW#3NXk_O`NZK4` zwdgJloNr!prB;-Pq*B}s<-(t#4wWDjvn28&FE@wRH??p7Xe=u-^bJIQfC&q=viikj z>q}048eDT#eJS8b)3L(X4m+e?iS}7w9;wDNy~$>_u;pkl0x|Gxx~ZDkk|+~>Lj)6+ z!;(s(G&D7r04JBxINK4K9$h}@1;&UzKkyY^zXr5TH+=;8F|}P=$Ag=|4xWP5$~E6@ zEaNi<9y%P2xiD*ISCVSvOyfrHFvDvO5!fTc{L%@i`rW zb`jzWt$QtYuMog;X8onffY-uxMB1uHF8!BeV3t=kk|V#ILu`x{+KZ~=eJA(}K`%I< zHwt11@_=M?lzFfqHEzh7L*%|8(d}wSZ0o+Y&FF8Si?h=$Ea@wnPxl)%q1K2y=!pCF zMVA>CNim+P-Yv03u-)yJBa(y$4>~@A8E#kYs{$iO4Gruo3DM%?4R6FcCe2m>{Njc8 z?#cx7dW%&T`Fx9ddqLF+1F)AqKK*lCj?foorq6WQN8~&4`D?!B^muwvxA{7=6{H&chi)nvH zd#ak8#}7n{Xdmg@N1#{&%5rEe)9B>3s0F)w55EFxvJhmDnfTe2J;keEwTQb zV>ImVu5pKl(U^=COvfF^$3xQTT7F?0>&#w`$j{qT zH^YFrzQWR9e^Hsj5QDs#3WLmiU@#DmOBt0o2$P2!k-#-9j{gh?kbG@+dcEOycWp7g zrULj}ceKGqzEdfXNf{FmZjcNA9B z!Z1gi4VFT$RpJPY@jF&8?1jSYOwEQQCf{-CkGzaL-XW3la~{Z%=Orraor0zrr~A-! zaCK6(gUyeT5^t$K)PfunDbSS8fmHkGHpY}Zjma{0RTohCwQw@~gjvfyOf=xq?VFu; zMv2E={NZ4&mBXO?10VkE!m{#K((tv`~HHi}V=LLdOu7GrqQAdB3hd$z{>R-9_>$WY>Wb@##T ztIy5>OY8tyi=6mx_BUrXWmK1Xv3U40eXZ5ic#g&%ZbN&BGJVdjZl5SHEWbJ#w!=$f zI}XYse9<>6Vy`6v_0_mV_Tw!oacfnFZA@e4Z!chsyfk1&K4NQ+#yjN4{|iS2P9 z+U*}h*Q#uQureZT+YVkOa&qIn=8AFgJJ$wiv+w4xcu8GujbyBK9c}2C+BP4*)b?F3 zz3$}n!q#A`2z@{^KJ@;a?NhzQRnb1yj&BRyCaY7WheLj`p`K!%sMf~2%vwO8CX2&r z`9^Hd(@R%V&Sq%%0ythIv{UQHM2YG6HHAix<<-V#1on>O;q1`>uY6Q0L zDt+&`+uTpu-CfSDv;F|*3-uj?m=?^)H(TAu1sLo5VqM+?9NHY;)gxwRr|R@!c*hPWb$X*gT2hJP-0j)O}~K9YHIS6klTbSLj(^u%|c%OLR@BN2+WobXJ8 z`(UyaVev)Ti7wosX6(;Vo=kQvRESO^<#0fIVb6o1>@kpsHi&s*&Tym)4_Hj4fNnc5 zpy)o#N$I7K16x4kB*{51KlNcRGz+O%74cZ<5WE}LuC9bK+wJ^2ksRRTM9n$5NdFRU_nBxzH5FQiM@>XrHS< z$WPAGSsLKSjE|!?L;Yxck~0ob?CZYnBbN6eP7})x?Ca7)zxTTv23g;8#VAdKi=NA& z*WsM^RfVCsLm$&a7A;?3a7y8;L*KoF5F`fqI>`F^`Jd6`Kge*KG~^mLggwTv=k0#| z^ zKN^6Pzrg42uv%%a{D7#9IMp9MU}Y=Jo!<+u==q<;0H1<^9tM<0#ZD`~6pmae=7c&8 zKRf9)ig<84Mp#WH4X@dzZ*d+6@rf#gX=7r=Nxo2jDZINQ16PgcdpT8Rv|n;TFX-_b zn)N;fh^C1*rfW<1rB9p1Yqm>^fB{X`cw*QTmkcM7VT z1DCPXT!~TJQhh!aYsyZ(lmZ$siSO?m{ zm8(?w(J(Ek&8al$vFrCL*Kab2?`IuOB*x5FE#E1QqD)$bfeOSpDOfR<%%p0_KDs|J zS@ME1J0z~YZ-3ylR>tD~0qheG#C(TrL9yD~ue0FTr5Z47KfSHF`C&~b1`_9IC`KU1 z|Cf7>2XzWHWolWW`9n{UznjQyIqc~9B)aGvCxY|SsDlt=F6sfO6(l*s&Oz?!d)ARZ zS*Z?ouktX> zMryK~{K7~z%+Hh#+vTSBiuE*7fw`pQ!`OecL$u~eo32ahO-GD(U8sKOh%xSK%Ft&k z`bh{e_?@nAk25+4KoT?=u>_0h2W!{;9x6A3FhfT|SR*tJlTEn@mMI__$`1goXGJe3 zX6N6FoL8g)*0%-L>#W(pqsoa_O428|e{6R#qvh8d)(?)hEq_KrLPCf)K)LsFI0kbA z^7`w(&AUAKPWnnV)8Rrtcz>xSmhsv7thiZgn0wKPrz&@n-<(=a{a4AbugnQkt6tl^ z67Zs!V>CHOKpxU>3DAc*<66k7s6LUO_PZqdi?upwW z09Puil3ODF!xRlgpa5r2bi23CdS4&`5kj2X>(0~LpC3a1Bn|TP3C!m>!rCH`~FE}UGjO`!Hj~R_w56f>jmS#B-(%iDB8PXja)&5z+ zg#t8Rd|KL4dd0~+_=QM86}A4!o_$LrKx~)D&+7Z4kw-j~nChI`r{GC-;5=9NhEh|F zd^cGfMW=oWoszL8mApLPSM>x&Lu)BU{K|M;A(cf!?(tz-F)qiHL|60)<>=`$d!CG%j z@P0e;>3bZ%`!7C!y6@x3>pT2v?O!FixO6?CU&;pGgz|qi9Z|;LjQAx|A5Yh~5FK02 z#Ug&nhz&dMMCdC^{gm&3AYO{4p|1gPD8U1jnW%=LMT0EHYli%zF$bnl9PWW~{(@$I zO?=0eXX5#IZ*oPo+zQ;gUpi49&f+?pD-Twa3+nR)OEn<5$?K2Cha>iMtB0U%Fq4m8 z0JG83y61!EY;Q6ViVTHRRw;F8v87yUIU5QzBU;hOXGJ)93Rt!96-%nAq|eYltj0pg z2SY8#(MN^>iVHMDQmK+L>#9vjO)3v}#!E(8bzA=1Ml<|R`?&$zr{vfZ%FvkeSaX>d z`L&fg51RqOecKNYMw0+DJGRjHUb-OvbuqIJ5sxz^;`zZ}Q}r&WjO=x>?8qVF;ac)d zg61Q446=XzP>C7CZ^s#YuXx`ieJ59@H{QTESMhc&u@Go_B~5{<#%wRJqCy~9y#b#; zJqblSZ}iEq;|g65@23uYdHDd24ZMqtYX7sQ@`3-Qt~?MWNye_3>zJoFczAgs(>uNd zyS8;E9K@M?;i=Uo8<(fGf6fuo_p5(J{SURcC*DgGBZStJRPEvh6!ijpZG07?giNh3 z%rHSU_*UsX)fAj;)5xq=kx<}6>hE|UDxtZhTv^IiKxS}tbTO4$<9rdU6)x>sJ|E}y z)%3BSUDiS5TerE5_2aSfKsu&ln?kDqfD4DniBUj*VO}Ihz=?(vFHLKxe&)yJk;_4( zCIad}l~wjUT^rVnyM-DBB~Z-H6~tMFI`uy z`t_jN0hv4aVb^T$i%(otCT&~CsI(^jdVk=SIcU8SM8(KhOnVg}@f9Dj#Ma!ZE}kR5 z+ZVa!e=#rc>`xb@sQZV94);TZe}0ya>6|x+g#s@GOdA65E*naG9Z$R}b@@0=WtOU< zT#f#PsmPx&j2>k*q7${uw{)=pA~+;NUcjNJ>Zk}SvBIL+5K!OBFvO3ohwt**M~b+G zb+069xHE<>^xheo!ic!l$cEGKV6s;Zl^XYNyFd(xa9Jxxa~x)uN}@jkO%W15UXei0 z=rdT<&q$oj`15$ZJ^$IWg{o$~J}8xaChqkJM#hp8s|!Bg1v6IjmS+)4X!ECD*-Gg! z;*8$h@f97q&)jjL))0aye(rfQPqp{zJ}yaN&8ubqE#K2m_n>6OO9SX42XtYNNCdWoIlz zQlGy6jc(j;7x8{wc6s@f-~B42aw+s3iv&AMd+WK)lVznFq_lGLc5kh|wFK1bW6@wl znI>7iR#Mp&Y8w^!;FkIt@SH3yFUNo_aML)eE06VV>_|T{xE86RrZ*K+dol;BgGO>En`)BA+z z+tSD7Zw7?7NU)H=JqCJpJpstgY2$OVgdbvLxAjIj0_18u5!2pC^BJ18Q{Jr^ z+pW0D7Xk)1*3ZY+J|n@a-7Tk!3FT4*S4096+VVH`bUBDlV;(42nX4V76cCE<{Ew&g zbFTyzdew!RxqtnX{Ds8|#1Rs$IUY_ZlSaHa=l4&h(OLjwMol;f$0VT#nupA^-gYkT ze!l-cl~b86O+U5CuUUW>@M!4ghuUUUucp#j)56e9~@ln5aFG zi6O81RC0s1TzhvXi$p|3^{uU=7Z(@IFC|0snX7>);;!2gb-R6UFMZ>LeSgt|_kBcX zU_&lH3i+0H&pLk2L3)K~kib3m-|3brxvH0s`$;LsGdZ3mnZ1ERvPQY-?V6P$vTg@#O@4n1^G33H?H{A zXl~M8j(vrc*tlB&W(Dohp{{Z%yMvmf!o*RpgF{3^Ejf9x9j`?fqn<*y&UcsB?!HI= z>MYGKsGS*z<_Eq*%>P`G!m%0xRj99K#l^s>~Q+}EA^HHyXCOP*YcUG^biRBWaig-c{YkTZ%47s;lb{<}0K}uN@(m(iZ1JlT$lmn8r<{$9>Uv3RDqS(U&L>ftdzd z`sejn*_VLFdItVdj86HKkAOk&M? z{gFJ8uZp;6XbOSyXjs<^dyizpQmkytFH#2#dP4g$ucWNlp+Skdv|Cs29tZ5tCUeXK zgd3#&8DMNJJqn7b)-CsYUzr6xja-iO6$FgHEsvnbbTjb)S0he(qC8uC9yh93D1rYR z1SqtPQ;cm;E)Ca52nh)6M~nqqYuCrQ5wss=9fS&Z6_Hv%YMoP+}!wKDO?h z}{+#Hhjo>CXg*ZNKzvLo!>wur@QfV)7Fntlh)g&$J(i zm;P_xyH9Fg+I>$b2xZ#*Zii5>pJ;!M0$n(A6-qlS>QBl_CZE51IWu+nO-wDq!~m*C z-u0W)3X6mSyLb%kXyMGT`CMA;m}s~V5cj!nH@$OOCkcm3SwJX|-o{jhH^19u)@b)=WSH@a z>3^6rj`WTio{qjX>TnCDC>p3(UOdm0e`~~j@d2EvW|iBrT%w)^QD9WW$cWuA`jgSH zfB^c_1`~y)pO?hgFxtMJkPe|ellTM;34PSx@#$_%2ERp4E`i{d8L-97(%r1`L5u=<265001p^$`(_>zZ=_TSvQn%#YzMBZ}y!Ayt`SZ$&4Hg ze`9&Qz902>G)8>vghGzjHJJ~*hig{V7Ro#A$%_1ggX|q&ikOIlh@mjiY*s!dqKsOi ztGvYE+ojyu;QJ<_`xT43mmq;Wlz)YmM4=lv(&xFT@x~uMkksEc7Sp|`kJ2?Mp!GgO zKH4MUXUO);WB;y5OnS4qSS7jAYwEm?E0dOEX{?~2lYhhgC`Hx#W3sX60j>YGjG059O%}rOo8v6%BpU#=W1ldhEIyN8?;nWs>!oONK z?k#*#eUkH6OKA~T9$i6=dMXkN>n>R{2!oz9yD@{fK4sR=pX;#UEf=>eJpt>^T$W-* z8OA~1tu$p$J)gTT12^|4w&6Ha7yg;mmOm)fH{Rn=FxXCrs_uuVVYwU^vV?bs=wg~t zWVkLa#$cEsP5(2`iW839a1N=^3~Ki-_vEYg7f5BusUENbWOjAZGcdZlZ#N1htsqc0^vEgmpZcSjnU_d|~uC$bjS0UuF zQx*g7ocl~$1*AW&IxLK35;@y2z+PXU-oXy6Z7Jis*m93b>5vB;>jZ$+QL=eoTd%>7Et?^OBtO%s+e7+?{qYJ>owFc#L0u466h$ zS^-QT%}&hxb>q6R`68JHkUOMRT%5Y$z3vo31ADv__729n3%NqPwD=hR5wHCNFbCSg zW`e)Z7bfOL@@Yee`Eq~N&Xi>^1e6kUW@au~iSEg*5UH-RRu97#KW0WTEAbNXn(jFI zRXHfq%yQR|R6QahWNP->+$UJm&c@i$L+I9A!#^*7B9k4AwW!UmZ|_JZOwBs+K(#0>22<56QB$gA%BKRf57_>=|Pu3>^W6o znecC3_DIC0*c`-6Z8nDPTSK*Q*!llTB+|qtaS#HBf>AkyJ|~x2_b`=>5|&_@X(trGAz zrZSA5F?-L)1$yYLwbLw1?e91K{-%sjai_%kyEImue-ZcXzW(zPhN(*wWikWWOt+?+ z8FEvlxq8|(!-sHFFf^>%?lNTSg!le)ms0O&EIU#)6qONs)P99LaeHv zNLD=Qp*75)k2Z%A5%s$TP3U$BaU zU%6$mGf6IF{DXDvpXTEHVry zPL`hBwsuE$T-&Ns^#nCLY15(D5aa&r81Jd;fO3EmGy%mYQl`gBJ}zaW#71dRM_SnN zo@;OEgp&EB`)RqUV#O~HvPnu_Dw*~`l?ecXZ87XQ^Vg)V>mjH6guFMvDP^1zOqZW* zG$S(w9y&QuXI1_8R56prnw{6rM+nX?5+(>Nv&1^MB5PiZk^3=J_$W`8y$@r3CkW3U zCL-_O$sZ;x2f-o_{EbZe5W)7*{kn^d$j{rCzK<6?3*RY&twB>ZvMG*#s1UckVfcT* z0*jhM1(RnCojPM`2&n6>l-KQGJlWuZM*rqqW?43@KcrvyL1)g-7B>tlF5e)X*f_Pg<}oaJyYj;A1+aU8dx#fRS{~VR>|g zc!a@bxipVC7QPnUtE^3Y6B3;6^YDF3#)9UAtjcV76$CLIT+dKVX-86gv@s`7ti?vH zZ(lem=uhl`9r^n$In=X>oFctB*ROMZp(z``C?#YB&Be$|| zj4Z<%-M6G0d-3h-9w&KJyPHvp(qFvohK30YOQAo_@bbL6J6-_Yc$q%l5c{T3gQtSJ z50O!Ci7y{TdLM@u=T!K8#IMHvB^CUlxth|6y?*4RS%mHb9RM9}xwdl6VJ))H8%|Oh ziah(7`SZm`Z!VZ}t*4mzT{@KB$u{R&vs?>x?B*~#sS|leIxLdC*7tq` z$j08x=W#_|eBg(6Y@seZVkLD?T9YLxSzd}(iGL4Zs^XBPMQuacj=33|l^vVVid^Kc zjUQ<>(9*CUK|BPn){yy?pRt33c|6gl;~_sY|7I-kiptE)P~u`zB6_vn({VJ6G6sPk zitCF|)m#%eXAW)1o+K0p1D_^{_=lT$g?19uPr-Gp+%&m$Xu3`wm>VPy2>OywR!;kg z?;wj^6BGdsfM;Cx1`AZ`pKy0S zmMur7eeun(VhvQr$omF(WA__x{-jji#n{ax8!Wo|{uwCjrIptq9BRLM2v5S{^S{Si z#^o;>Y{>F8chcmQLHXfY%*TX5X(+*$N{+G?{@_!ga;?*guyyxYjJNn~3E8Nci4%$! zq0fBnN07t@m$SHQXBwHl>zMzrv6`EcrS3*G9-){O))Lv^1XYqPOl9~=KY2p%9x&dS zWv67YA-&(AjmpQ9Vg{lR+>745sFHffsr{v+H3Fepe%gAw@M-GpO+Z+@ffaoe`}gS2 zJGVV8nY?KQzk@^aSp#B9!@^5jrI2s~HK1neA?n1nbN z89sv0L)D0T-Knq}?p*z;xaDP$(j(Z3R5W@&ijQuXz;OYH(s4g(v8u~r(=i;IsT`7{ z4%-%fh#c5k*~U@GDUCd|#3{HS#{u(; zX<<0#=3jeByB(y-O)Ow_^*mIvxWU?}Dg4=m?KiWA(URgB~&g?RA_WsdPXE+WZtAfsPX$L3G11q$J^(wvz z;gtD=D9u#~V{e49D_1B8sle(h_v?3R2cEw2raw-Lp6zTfDvYxpD?fyqd-3f$_1Wol z4uk9SS_y)B+)DAGaazp{ai*1)WbxxYh;XP)Hl~uauWHwM^%5;=nU9d-Al5bMlIsCM zbNHe<6omK8tJn-};?YPji7ss1)qf~HDDCG7n2w`dlA{ZLcO}vosaMD@@g#qn5vt&~ z$%YFaU8vN^e4Za1C}Zs8Mm99xw=@6=>AA>tR{B|AjL>&oxll&a%&Y@F48v(`nrNSD zjenpFPe13&rRpdYb0m36aN4*I{2Y=YX-h-xtjtMH_Nvp~bZHF>-W`O2$QYMvjVxST z68~WgnWlXIRFwH%25iTng`oyteg7d%3E5jd{bOweij-|SbCM+L85k68I)`jp_UgNX zwMMGxdcvz+`RuYXV{@dWbmS7s*x`;DuyxxJp1i8J4>e6hzp?b6^E?JSK(w6T)vJo% zlH%%o63$8zfD}fPSzLp$=~OJx90*;%a*pWntp@g#%jW^xCV)PQ+~94pc(2560DQk| zeS&P7KNxehshHiRK194sqkC3WP>ObytSde$n1aZ&kF_y^+E~Df_QyO5p!! zx(b*!zqk9FFl;c~VZ)`k79R{RGF*qdyUTF5;y&EneYpE@cX#*i<)3^_mXIZVdwcun zxz9a^K*ZTtBRT3I4@SwfNJY*8^Uw^cN7zAj^hksw7C{jb%p@6Rf!1N1??+2Wvzf@< zH4guAg+~NVB@{y~QN-w%>Tc%Ee=bU+a(Iz5|NQ^O@`bP=Ng1_bGjw{Y!2WV3Gb63|67@BAyo9FyahtOTisTD)bV2>GS*O67zUr3&Y(LboG zJD{s>C#pOtmu1r)>X$$pt0P*HCIia`(&4X_1i@ESLRbD3I!+8pPL~iI4YIbAeF-rT z0uPB4w?)ZPk~?7;nc^(7iv?LleN<9GqyABOrC%#+GM{w6Q#mDTUdI}YrjZ_}k%<3k zB6V>R>sV}k&_~6TeUh)z(#>qAAe_!fvf3k0Q3NyZXi^QWam`rG)edL{biD_ZUMtI4YQ`X4cChk> zs-xPaU02qx&e7l*5G8SB^U|2%r0C}Ng?IHxEs|EOKU)o02hWuR+)Lp0fif&FoYyrnmr7Gmi_K-XG8G{Pw!N z!zO%<33iNZ;Or!cA2yHdhU|Uf9Y+_jSsB*)Qh(BPa47XlLtZM8?@35a)r(5Da+5Y+ zjz9hy-8a8UB9c_mI*pNf_OSGo9Lj~Cr`6Ld?Q`Za&>PRCu`Pd;A)zdaW&s4bfY!v=J|7T?JM*dkde!Jr%Oo8! zmyL2%mqgCDf3Vnz;Zy>Jw2-A>Yb?wXtfZbVxP2N9)MMvpI*Mdwp9+Em1`x{=@icXe z8u#j#u05>0qnxcDxCk4Br!m2a=)IcZA~=fND|Nt)H0oEm{0^~;#Of~fOY>!-e4uuF z)Pp*9#+|1wE>Mf-3}A8k3M8CccSeuA%eDc34VgqgAsMmf%IDr=CX_O9TGYRU5kv@a z88v6kpO(V?LV3mh5Eh8O(28ZCNs>!88A39&kJKb7%HsNr>=++S&V;yY-5ZqyUpQPS zX$T$)g0(>2#TBm?mQ@fVW|WA=CMVqe64j}OB^(@vF@qzin5iKot3)OURg97t5nvKE z31-7m$HUML9qf9@HFBjnl;ZNUi}lwM#18eDR2sg3F%!o={uu2W|4-=C0*$Wz6VVEQ z{uTG6{pvBB{5Wl9Lmu#4v;j?tB8m>ni^3PJ`#sRG)npwTh-;>lZD=>iN+QQ+<)UXX z+Od*2Rj1p;z2TttLv~zBS1o$O9;9nl-Q&t>WY*lr=vc!=9hybPfJBBmrpY#^>4QU8 zvFv9n7cI^Jn_V-f8Af2Xr6$NpQ(+?6_;)C=CT~z8!cz2T^|$`7%Wdq>DU-I3dEe?P zrYAA%PN_tcY)PcVFQ5cgu}u7l<4y8WiUIMlLNi(CI^RaUJEQ%5t7vGl{x(?^$C{0u zR60CS$q+_3-%Li8E|M?zq*j;u#oN_~KHzRN z{KH?@{WtM%0YYLDu+egR{Y+wXUzU>OEEI#n6Kxsvies!* z;)Zk9RL~}Dq7Vjhn74YZ8PiBe#22Y0^cXA2#_U|nq}r(VxcaiFf@*W9c)6%BB`;w- zQmh%Z{^V{c5TJetMoAoeHLqf|ukGa=NCsvJG({-mvJhqsbqETtBBBK03zr zE(^^FT{NR=&@ZwCY4)YOv0!Xwqv&kama#&!t|BaFLn`rlEDd=_p%rj72R;qS4qjXim451@V93i}2f@N(kKZowpL>`8@BQnXY;;2Us!XXbHqRW@t_Ke&1 z*XN^?lZ(tR9zp+;rvP&NSAe#HUnDv2e^XTL7oheF2V{W7kh^BlVv=>caW{QaKH~uc zAXVYF8+v)d_S%0oPqz~z$7_Uip&2yJ*1WTeUR!6Lwb8C&7WS(Ki`XF%cVr8p$Wz;a)ooVprI|C1neRUr zFi7Ne8GX9`6leC7E%QVanSNrEb*hLItboL-vWj?J=2l2dxdgE!SDHqTfaAWvlTZNJ z*5T=5yfbA;GvH^yQEYRO%+co-#}R^$L<&T5F@fZ3G^?IH1)&n4#>2|=F7ckByeOf$p`|m$3bZ*M zkN-hu@Q2iH*>mrxfK1Y(!Oms%3PQ9A#G;Tp_44k~kiUYr0g`d0RB0U_ee<}O%9NsU z>dCUE_1hoDrc6X8CC24epi&G;huwgh4Sc){{}b^0{+StNO|Y{ zj<^{2FrnL=WGpEPfGEIl|Ejj7|HtS~24lmxP?*sJK0`2#5G*Aqn!k5A=?G8z&~_Nz zeoZsBSVcgo1fmMgDPh(MxEMXfLhmMc{h#UD?Jh* z6YjPNi-kJ19{#F7noP5M>g0FlKis9skwbb{=!v5NmJGKGCk{e!CyZ$|?3q!D^;F&# z7npGwai)FdE5J74qPd7^E!Ja5=xLc}D8;xm@K%wKFvTiYp*~m*I;%fOr&nsb0Zt{2 zYtiZ=OW5+hU3aLcEn>`aQRyqhOG3tyGZZj$t0f_j6Iu)MB)qNywS^EfYqLMmCPjTpmXk~+!$fW3quIS+^ffxvI+-lN0_cB&$0dr*Mn{b4;Ob3wLs zYe?tWKkM$3Y1|2c(UOLNMwTy(5@-u$JzZZiHol~{{GFP9cWX)YDcT&lmLxGH459C> zIE%;v5mLg^&16xbV8V!>5y<#pfB(f=fat4Y>*tYE1`nIZeoBcw@Yz z2@}Z`HAlKK%ARCIX^CQ*(C=q#-p2Vep=NXC==6N-^SuwG3-|RTX6#R7`j+meP!*dN z%0@eG-DvP=Bta|2jon;^%izT`zI7d)2bMt5zA?kR8TFWUxt8ekR_koJx^p&u>Hen? zPmxKvNnAT|1A9N9R1hp?H4a+8s6(c25GVjm?h-Dp-}B-UU$Z>7_BA+ zNUCs6i8ZIYZwQ~Xoqo1C!=_SBug@*cfD>UASS*fNQCfiA%va^NZBy=Ju- z8bbSX=A*XU1o9@Xh}qXh)w_)-!OUFB2o=~%(+(Y`{Hg$zdqXDK1^(B&#~%O8Y+J>s z+xzvukkj}D4?S8fw+&$;*SoTx`UyxsF;6(#8Ok#}oI<c00Y#opXD-HAE` zV2e`7OYjz4IHlTy76Xpr~6 zJgkDw+NRM(e4qSw_OXy`?w0%u@hEDMt?lS;ZNDq`nN#p`4SZ`+TnE77QC;(S_hpmH zZ*~Eysy}fylh)C&@-`}H8{7IDdFXMG2r27ZA=(@&c(eM|T)CxfSkC) zCD0Ht^X$cGNf%@T$xbq--f{i))b5iyeT8B`*a93Tac-{yV#ol!OI=_@ zQ};0`AIC2V^CUef??UF(YT6Jc#Ac)n#8`3~R!#bbBg|1!!cOs=pQ$Gn z-WOgfnLK#L4Bjc6QSDG+rAz}e>TgX_ss&ER338jgy`&GW6^8; z^s{qui7t@OU8Kk~coX}oQiA=7fnAq`?R4g9L(B;yNw=f~5ThMin zsQrWGu%VBcIube;!Rj0V)kiZwl90>1*Xt`<{APpKBBl9aDIG82uiMrdEme@_vlIfzf+jTlM{&wW9pY5wIm5tgm`r&-6Qrcbat__IYh9 z5(VTuTHIFtKjaZCP22-Qp`n{F++$-d4+~*2h3h}1Rhw;tjmLNAdoasRjxTj@WrAxr zzCK@+Ti@JF@w{BIT(Of9@h$E+CtGJNBUA9LK1(GPc||JI2K^Oekd(HB46eajg^?>ZQAemf_~5`wHDh?;ODq>eV$tP z8I93b_>TjkKWX^K86_3r^VDRB3X^e$6by&*R);)m$ zbNk4R*d;PnjMM9H0E#mQqmtSCfVD4bUF5>Mr!UyBpHR%f5MFdG)-FL=MF2Z)H-$;l z9@IW6y|1mJUb!ZtPOf34CO{DP*WoC3wv=N^`Db-)1#L zZgQN;kjW-1-|A=t894-Cc zM>HBIR@l{!4p)fk5Tgr<+p4zv|5<=ky2?7!)J>B06mQSiEdN`sa`SB+*KaK8II3mV z45lu>o{jKlsG_fkVCf32MY(w@+eJ$}x?D{7bhu3>U-Lc6-(HHIp%xW`6Bz^C6WS`LxaC1nU=Lq7xooEV&24BQ50KwqNtnY`sp;_wa;WbXTVp4=G!yA?grIG4sr;;x z7pcXeJ_m+9Z22b5&n&5WmyuNRyGUP>48t2b1y8p| z>sgdK<8tOXsD!0=lOxoPHerVGUT&@3s$l=E#aVWj3p4Dwe-Rx@2|2%Q_V@6IYOtAx z2q|or>gd6BsM#d^D#pZM#sDFN692TuY=w@WkiWqSc13ajtw3*XA`DT05jQ7IRkUsV zxrds91B?#q$l|jXHW7}|a$(;2iDFC5=euI1ao;wl#*DV>TL&B35`#qLFo>0nahjf$ zrHihm^Dl_qE>lN%<@zG}NbOPu2uW18M=8g}u}Rw)DL*@?`d3$yCSstTLaaH3_-|2V zjeX7No$dF-8rT;2YhL&WjRr|Y26%LrlgniR7sm)TC)%tOA*2t_qqCSN(_>l&({xT} zL*gr3O8uIU$OlD!i#|}u0|+l4FJuRw6}4EOaoW#kuQzPVT7$_cfFaDt{d&hI!*vM% zZV5YMV=tbRFz)HJ{hXokcuPEQiW-oE<5)f8;(M8!b$*#NShX5@b?+AzQi51iI;4p6(TTvJ2hCr zQP$vm|H;Ubf3)=wezB0!3fG8}swf~)Ih%%JZxQ|$7nnx4?jyD$6y27*)Qs1ix1KQJDr>{OMG_npahG=sD^Q^NNmItG|96 z5E&-XIxFOVLN9}i@LKWpRm09U-Y>D8wC=LozC$Fid7or6j2)3SX{!#I;IPj5Iu(vN z(_x4)H5t-UV|*Oimt`x7{VDE7b+sYJs4Qc*$B5y4avftTys+%^{#QDxk4tqkdZHrk zr{|CTU%oisBwgk-vq-kDG{P~52 zjRgqyx||<(;#rTG+inij=i9ptv*MbmqvB3RM@E=atUD`ui%-X=_e2k79-iwTUVWOUSlrJh9bTr;@d@~w z26oF^f__wS{!CR3<(!SSCmlG+$w|fU9jrQ(kgk(EIj+Yyo9k3dcROc3uBAWrT(Z$9dz%#TM}fFh+1b8H zK;IV%_ie+DeQ*8x66f{*ENOX`u66{v*^8_RJAPU8c=-B-gE#cBDKs$S zNBp-=`C5btY$p!~4zOM=iSgR??-2y!%n{b1=k%&1aH}YITJzDft47XX0nubf?qAZI zBi+y8;*Q1LO^VK~NlVIY^}oDNp2_5G-T0BADdh0`@eU$X0ha#4>aQH}MNEXK?+q1u z`ZBn;%G2CPfzkFS4g&7v3tuO`?yJe6r(UZkE~ixo>{It$?ndWxlD9jqZ&EwJWOT;Z z{Y1t~In~(G&|UZM`V47s8UV z)8zTPEwCW5Q!loey6ilf9)w06=EWbL2BBF&xuk^Zz)a&)^LnRBfPuGzH5~|72&$RG zI2!X=(HPDtFMz*E)!ItSu7z?=n#6W9Q5iGCYoFryn5|B?#|*Zu@~B*hZAPj4`Va;5 z^cmAJo-MnvO{;s5x7e>?TT@^7ShkTI#H&U8LhfCw(U|+Vm{T-2LX#&iUK503hUsM}oNw^E!e)>4Ku+X%v32-O#53Z~brSwH!OosoGzN)`l?rR_1 zM^}I9Uu^T8G^~MQrI@Lko0nZ?j(G*tuRc=Yr|{-LpU(In`#$S-XmjrjMr^mc3^|f8IYyoS`lZa3z3pIpkq1wQTL{WWJN9b(1`g>gw|Nt5QpA-b40YU{P~IEKHv#NKaP z=ib}HKsOs4@sl?~)AVB9aRzP3l#RF?RKiOrF4-6jl9`^<^`U82Pvqs+@_z|m?;Rm7 z1r87GM|W;qf7GyyUh-;`Cc}e1^g+j!FQo#wA6pKk_%9BSpUdS0UIW{&_^mJKqa<|G zZhxLo^-QqlW|K0@pHJ!!_Br(}O-yO8Pt77ae!lV}MP;<}QAKY2EwGjH8@3th4dvl1 z2GfvRaWC)e5BMC)4}Hl{*CP!U_tU`Q*HrCS!dWWs|7n;z&ftp?&+XOB9A1v4gnvbb z%wQlZ5uI}$vSopZpw+oojN<-$KiaBm`-Q>NLBdMg$<&B;dhJWSzdO+=Y%<;;`kQDo5pHRQLxY0JkZq#gnM&62^fg&WxCKVIkq%-c;dp$YTwBk|z zBFRQQ!iDcSExgDMipuRCT-k81<8mp_B09k=4p+wVXs%*5A(L##>z5!NC=LM*t$b_g z3!%o;g+3((b+08J2U68}g`8+vI!m+W*3GVCWp_j6ZMWCF4UF-=;=xSti=vUX$7@{k z#=`?6o5RJa0vhXn*~`n_gNap>JTFHumXPKpuO~-9Cm5~+7*`#r3WFrjpFhmDr(4!P z?(*&S;_O^<3c^*VkMZyuV%c3~3%s7m(-x3>lcnctmVXZ!d`)&=;&b*2g44{c`_$bLhU)n^^7Z;Oa6UjmC6Zk7^)YRX{aXGI4AVo(Xl<*>WBt zOm?JgV6HF8McdTog$m6?BZzVbt46Ylsx!&1eNh~pjLZt)`0#N*r+qif?nrZSbCJmN zwixEQ9BqsKw|B%`xrC)Y`C;&EKTTo5@8g=W0$LX;6DAvS?(C3+FYYKlxQfFwBu#~z zi_#LB1e_GehAel`T7}&0wleMP4;f87A7~@()vrjF{|;ACr-M94K+u}JzvU)Z*e{d5b<%v=%h@-?&^~;Bx!dS?j0dQDuM=8%kgi_jaa{6y zZISXk4=dIY#Vd4Lmraq&A{>4cbQLK@^b?ZNm(TnIjJ=vUZX+Q~mLM+879t&pz!*Iv z^Bw&Ox8`G6Q56cBVC`fhRG5;4`Is%;vVrB{>qRXgC@MPg4 z0bH5XPTLkk#I;RoDY;UA$WGb$(GhR%cjJIEaF9nkfu!%#L(N)ph)a50!eqF7=)w=2EOPr6nc?#)p#t zngs9m3GO2rg-s{H8tRA0JtTHw@LV@Qt78DE(;tGC#Fu&~*Si*K7M(n1=T_;bki~Qe z1w1_IH3FkurA-cAw=eOhW88Roc8mddikwW0k_{ZgyP-zVgd_oV4^sAO7%Mwa8Y+yA z?V9bp%Zuo0W^Jy2ULHl8>pfe;@(JQiKsuktgVu9ciX_D^>jMp6Zdp!dv|ww1x`gBf zgZ2FKo zkNiOB^EpAth;dNX>tiGw)GUTtnk^OheGxu^@EV5|3IZN^c!Aernv4f)sP+~b*r}IEnY#)3(sK{ zN18l)UxQ8Z8oTtKUCLoiV!{FFo zci+V;(C-Z9Lgwo%@RygDF|e_-sSuEm`Wo+d6lv9J#H_YHF!JyW1M;^&E-o&d&wH>3 z1_w9Vyk2427u7-`#TqOTim~bo$pl!!c>{by9IieT+yml{`$7ta#6OB;w7`s zzY>Gp1S4g@r5Q-S;96nwaW2ck5NB0-ubzHX;`F(R?BgMj_K>PbQRJw^{!|c_&FZcN zM;pUIx;wGU+rzW_B5ZTksRm4N2~4=VY6vKqszs_yjmR)`yZJ*#J~LAoZezu4*{-vy z;~pLA^@ib^zf3*Or6FU{sdTSDbPdy{3kcr}-pm~7_2)5RAfwC-Z;8^@Jiw|bF(>#O znHU#IoFuwAKh?Cj-`l?T{*RZJ_wL#A$&-I%WJJSmge^8M&LhjLqwH66L0OsMbdlm4 z{kNYWF&8&?VR<srC+sp$#6CJL z4RMXonDnHGY5jGtSA9BUaYJyAfMJ{O5e9k{qU|tNh;&kW*a%nUjwWO2zG>G zP>RK~#y@?q5olAVAn>%Cv5_6i8njmW4JS|3^TH=YLE)jYf<9?;S$;H0A zWMU_4yp#uf1bt+qC%U?~&`wQi(Don&8Rx`~JXEUb8!R-8EJaqYgULl+AoP}%zS(~` z41<*}AdALt6Lmun-!5MeOfRv<@&w!I&j*fkKgfHEgDtsF&EFp5=)lkpCc#<=J z=bZMeT0B6{@~$$YZ@RIja+Y%DE+gCo(lndHkCr+ARWO{a>LF0{Q(>VAWv*6ci8dZc zNn_}Q*R6kaFsr;ga&S=M=45#YFq69h8Bak^Prl5bLGkfef|)4Z-+!m35&&0iE$d^F zl7gpAdo1g-P4G0!6O360pYAWC$;2bG2djqn_NWBmOwG(}zyttKJzBU(aF4D^sZu~d zfU%VoB^z7Z@USE^3k%?aBy4OBolj|8VPRosD{iMuUN7!NMMYrGCvd(@hT#B|987?$ zcm3iw2e6`ASXd}1D4+xjS%V3}A|kS=5JV60O-_iiavFld!}BZq6tbw1LyS0$!v{Z8 zp%JBYw_0nE_7!JOZ36q>&tTJ(e)q9$3vxZy<+>5!| z8@D~&q)@=qQ8DD|QOScfL|UOuoee>9l*kEeSw_f3qJ0r=F*PcQScCism1(SZkf1tacgN=O zl^bkDub_Qc*dKTS0aNzU4<6qTZCUbcW|fufDq4g2Pk4R|o9b?FD3}+08%p+Dv>>(& zCnc6$EJca?NJLgfC7DNuLD3U8<`YJ+f$INoqU=GNEmiM(xIS!r9xbmjsdP-=q|uG99M#4eV7=Frj@n7vbDZtfmqaxp zcN=?Yjs@`T9MEs1KvGuwaO=D=nOW`Ms#AIL&^X9)<)?qzzuQ(cfzjTtYt~?}%LSRg zzdsozCHm)2W1*x@4i4+*&i$jKy{5IcOkYJ?O$8)9crA!OF4kL7Qd7eRc69*bPW=M| znMFl|;9wbIz<2@kF<+<&_J`0LUH(K&OC#3p3y}Tg=+`4O94&Sh#!35eLty3gq#nuF zGddBHw5&<=eT%{J$C4e8Qh+3sSd&-hbx zK(lcUuC1-LXLVv^p$xpZuw|9ne15Hume*uZgz^7;ja+r_A4A*vL?a|Z(atURqhGhv zuxf%oJJAfXOb`a&V$l^FjB?z1d}Q%ecQ+9BZfRqz()oP*Xth03-{}*jXf`W5E3?%K zkE@%T%jF0MH5Ju20%UnrRToWKn~5ypf78tHRRmr1TJM(RR+wUGycZ{0qi$kYV?4?v3&AI^Ar8a_z8W*M@a(G$@-Lk4gGA zw)Q%H#EztjTUyd{a&qS6=YRMqE1MZPYg(JmF*_E=+cqb1A(vTM8GUkMDJLgK1p?`9 z55}ob=)d4UJ+rHkl9B?M^5F0gmypn4xyeDr&=6|QK(WV&nz*x*Jo&y%EPb@P{DsJD zIKlw@y#S23Phf%SJN8?^2~6OAA7bnHsCMQEAqy7-t476Sp%Q`wLJ--AL8eSBQ>JM- zosMPcdqY_pDyeX2DiWKw>;$*QsCRn!c20STa1v4hye zor|RkDkF-n@Cy~>Bo;vQ`oyqVVl-69e<~sp>*Q5c#7e3ocp0yBuu2gR8It43bLv5K z7;|ivYauRcFOPs%0TpVs`8+caqQ#`7a?8t4k489B#3dw1P~cF)MXqgAUTqHPgh)_< zauwLItDEaeFkY@lK?bg>frjHIPl*ypS<59hIvVwTwg6wdISiDnvSd(l;(g4dxPwd{6tcDBjCuC4r)0!fe_q~pY1{=jv2-ToRRCBv=lbwWIH1tsrk3h*2_ zYzkS+nyWC;4Oa}Wx@=%Kzy)*5&9?s{Cp+v@7~;LET_gX~ogSaNVJ$*kQQy{r&nwZS$OeONVFc_58h{2i)fVhp*>j% zp5)}@vI+`0vt@bt`BT%={DY z1Z44SZTO@^19F5b2ALl}=|ydIq?r>dc%>TP&L@r-DwU{ow76W-&(ZFldgqz^`}c2r zeEj#=uAa5CUm8-rICXNcHFvZD{D`^W46EciPQiP5@cSYW+IeBxX_0PSV&fRbFCg$U zD$ONx!_I~6%kFXg!R(7rU_u(VqYjiDT0M0)7EcOhOBzlT0+>khrSe9wjcSz1M@mtN z(6-_zvU_}w7FsegJYQvP?d))Iao_!^u8w(natG>RSY!h2lch$g5J6Yhhn$!??~Z&J zsj0og!_DC&mXcr1@$s2(h5J*Cn3(92j%&;*cc#-CZ4J&$hh{6p1rg9J_*8oCMqHQA zy~8DH)|idy1SsQ^lRkK%c=WepP=VLoK%x!*^>-3HSP10k^xO?G1_7cljpA7JLlO;2 zr4&g8c{etEi&F_k1l?t_s*pY(H#^#V;8 zV-!yp&F)9Mu6TRD!w~M!t&B=bV_e&O)pBy;2+8_3RF(eGq4HPbZiJRB22hT{eSCj) zdrRsEgloN8HPsqpRA9>ribBSAv-9%8@ucfSlQ^aWsoRdsowGN!#b6X z)Hs&XI4iz^3Awv-PWsD_KHLLyy`GD0pfS;@o(~eqeh3>Oou}p|DhaTC- z38elv^@Q8gS?w_fvgD0V#8GTW)C0ZYxts(%%MrQ>(Hj&y7^;cUy7??eB;3=Z#yq|2 zEc`!8jde6F3wl#!TvXtz>xpv&wuT!V>=@$vWjtsObXR*!FnUCz{FtB;V2cRz%Q7V(4APyD)JRVr3h;I7ujPg_ zp@FLEDtuTKi@kZU6ycWEMf#X1%aaC$qOn^nbhqBF3=ujl=`_61f0vR%Qm-@DS?~0J zh*;6BT(fNiHVK?24YaUgK+q*8hXD{d+99uVwW1=4ZzzB2>l2!?`;3jv4JLf<$r@9E zd*`JXSX&vlxa4T-9Ixl<>0V|aHF3P^&=OXWkF$pAi95{lTlUsjqB>xr><)g3^dM+n zZZt_sQi8Ieh{z|UV$8_C>;wT1Ikv7(pX8Gk7Sz(6ANUhB)g+9_qz%>3EJ!{57X8K8 zi4lK_vXQ;3`wd_+?^Jg}8mAghxt+YFC@^rgeOm(?3_IR;{gEN}C5r63KXC$XxB`9n zf9dWQ;+}wB^2Jg0m@QB?Y+8ez@3#|NwxUJv&bq!N@p*8$-5jm&q*(3Top0JZIoWj~ zaiAdhGbTyKNCUS9U#>F)3;1k(loI^7mqVg7#6o(ZBp`Ak#71Q9Dl!{=F@fL#Ng0TK z*^pqAA(Ndcc+^-yTWTU|8pk3VmFrQok+b!;@gqtcCMK|@&#a59t6#_t=vy@zDpkTJ zC*=?j5Q2nA^bHJ{>~@C7j>vHF@JP^t*Up{iT3lE>+rLKk{V+77Fsx4M3j8v)x?0}1 zdD?VXCQd>MB|{As8fQgnUVh>5ihM(H+}unoEQF_6CnqP*oOv2F=;h?)Ic+X5zVJOF z6v4CR$5kvx=+~n`s(k1D%rSXbornb5Bk%2Y*BqP0#l`QfJSac-j=e7`0^Z-*;mg+2 zUK|es8D2c#m)lK+b3~NJ`3FbkiaBo!W-{ss6B8h?@NAF<2q^@3uXqx9S%>S4GoX3N z%K?J5F;K--CTi?N1e2^Ek@2!)^$2BAiF2lZ{-DPqR( z_n-WLO^Vwk{d`IaZuWfq$E;PrOT(98ywJ0A2?4GNp$$x7XL_X{YXiVq~2QqF>2LNk6E@2Mdw- zb?E>fm0WaDTx%9A#gJ8`8=Y=*a(NF#Ly^x*f7eLaCl>20e#CEAo~eZ4m#h6QDq3&KbDVWyJ^-*#B{Hae)C1Y|Hu-;E^QJZ~)d-Q(v#goCMgF zhR5|Zkb?E%!(0r2JqiyG|L7}VYHA9^j6_Ka4B_?6K%z-| zj)9%?^5SP`IeTw5Q!=sbDMc~m1-hD>oei8eWs}~M-}fUz@OOSZu6KF4-+gyC5$V~T z%4r7}!Gqf;SlDYAPu=IzuJtvL);)sO%S8ewu-x+28$94JbFB|ALt08|J1@mjaEf)< zkfk1=6-xj2G5BLOkTQWU3IE9#Fe{z&$VJ^O%601`MQ9<+C_+uFZoR=se*uITWW`iD6)10OM-AsV!f>c6}2gH=8Y+j@e93 zPF4YOXKrpTV3p}cB6j{#5YPe)s7udT7rZt&FmU4GgBZ+4$}|#sEF4 z@m?f#SEB~~W|q4eQWNbEV9o)7fexA8v9TBkh=?YDT$W`$aLNaG%dSUVYwIs7^#Uq{ zfq{XqZdFM|Mdm9lN$cx6fU7t*r(uGWfFec^2xP~{#{}GtVKXyIz{463%bF_|kAeL< zAOG=XKc8jO{$=8ppMk+@(hT^PO=!tv;=`$&=)``O-y;dRMQE|~^z{Lu>$0I_O}FD( z0De8Rpc~6=X-R#0dh(+^RKeA9bYw3o(yd>tR4WDAc)jZRpNet76t~U6KWbnp()lp} zvgC*I@%=jTO{593WZ*s#SiF{(9t0!4CV3<`TF_-kX+ZFHz;>A zlE18vPl_UjkMkR9WW8GHLr%0ok~5Dq4^f|KEi-_(1I&DSN@W@Vjt98{zc@=(0!$#f zm3y{1pWa?yzl3C=eEt+Hk_`mUBo-qaLPFtbg_3xozV_zRwnfIh402yhdHL)iix)cX zf%Y2(BW>Vy29HM|{)STTZm;3EOCU9Q|L@h|MVL+0_xFD<#=ZYN|I?=ocwc(I?}~~w zc6f_s$$5~!FO9bm?T#m7e{;qnjWngn6%^C1JSZa`mrh+XZ>8N=yDKA#v!&cu6?Oz~ zU7uj25AFp@Q&x0@Bp6qrtvkQksdAqRjy0lzZ zC^ef4`55@h*k0t?U*+FT0Rrz&XFX3}R@^V8UXE8^kff#(`Mvo510d5k3HhcbE&ywV zC@NxB(Ov?DPL<83NS)>KARt-;V5cp>?cEkY?8p_qJ3EqZFa#3>_zd?+gD-Hl7ZVrP z)8_FA!~;X%pg?}3v#zG*4Di+SP^~8)?$Z`@)p!##v&|%<^!2@o>?a^4=>ka1+_9r{ zI?q7$7~c?ZCD5i(1L*jG>IT;nc$@Xro-EMVt28^(0bvfvv!1VytJNk`6!EmGK3P;H zs?`B;AZ6)+l4o>E=`%o^8A>V|0hD8jOnRRHY>nsopZw8$HLTPW&>iOiIPz|WS301} zWCxUVmkVa)>yFLm&OGL;ZE4C{9&u)6jR62=oXlp{@J3HJuXbq-9=03f?K@d+ev7M^ zn3zB)y;!mvtMq#Hqyd3m51LjA&6kRbi{HE(fccDoRt~7!R$LG1Ry-dqR=i$4fFKWi zr51p^9Ek5gEPY#9`{VNIT!qdDpe!KZb5ATODM1WDmLkW)$9Dlz>pg(FcO6U@N0S8u zvFN-y<2elYN$M;Xd$WW?E9T7!_&rnKzQ^%meGC*T2!Kbx4~MPk>>UvB30RbgvGJPL z%eL0u@$vTAdKb`H%F0f}5U+>5KQ3U_Z&W6rvx=9rj(EwU=oa>kO-Nof? z0QlFSQf(LxR5w}q`JwFLhX+t5<=`Vc~H zM>t}?H+F_@mN6bulIS2~Q&Y6^mWzuO^6C>X=MN>oEtBX zr`Xxq*&E}TKe9421>P#8EFg0M2&2~1d88MHAl&&zC_@M=k@I&TYfel|5T|=SnAlKP zTP(6y0-H$ehjKVujt%>T2*d661oQg(T6ZX(?(Gp82Lxd2?{5!$aK{!5Dl4Nu|0gd4 zPwe-$aE8_^=s2rX%he>LbN4cMH{DGd*jb<@q>uQ@?cX^G==^sbU;I{2vDd9t07K;!l*On7{C8quf#&VULjC+n_}8yA=jZEL zhTzITLE>l6h-%MgSHKYR%Yn-DI;*-`{*56SC|4%_{Ug(KKNkkR2LI#ynBUm$)NCcT zmb+{!kBc16(`@4&y;J^_%gVqTocdNe0^2-3HT6SNlZcRz5I7b2FDfHL#>@;#h^J1_@g)kLP|{?4D_eKk&Y||OkkIQjSbU}A3uP_K^yIM zr3XfIWn{hp2}tYtK$+*FpOlrA^$!(7T3VW`)fMozrsifKx+9B55D35t06xTGIJ^e9 z3vl>VzjzGv*oW5#ms@hV+1b7u8@m7PjHW?=Z-R(L`=_ohUYk+O((;7d>n9i>Ish>2 zCtx~dXForK0_U?W5V#CSl3~6B)r*6J101nRlRY(Hif_#E54-qycm;)pnR$5uKdD%^ z`~I)6bB~5P4db|NOE#Br%8Fr=lTLP*Ns&2AMi<7V2IX2|i(;`F*RaTLDwobA%p_x@ zFr3bAB`PzVGjOzMt>&oA)svt~s3_ z8|}F!_Vrw;{sxC)=d8ko=)L->kA$ciL*zkH^zyT+9z1wE0FI9OF!Pq0cgh%eBXTr*I)9Kx}1IQ{#zQVgsA8A8!F!3-u+Zs<779 z(XsI*%K%vh`=hOm4#Ak^bxt#3u|;R~#FU$*9q!#O9Ei~k>oORI((rkcjVX{y*s;&1 z7UpRF)i2t+x+W4yw38E0venepE0`m+I`4Pv*~7msJ57j0zU3YvwVsGpKbmT(Bqe$n z$nV3tXJ==N-kEgz5oIzo!>~DaOcS!rWX~S7O>0(MP^e*p!BB9Qjx7ZI)tY<#`eniB z{tP{-npyC36FkTvry!rmX0zkQd@Ow1l!kg_yz?)dvwa0u%Y6kZtN3TWo%fHYnd=Tm%zPbT7BkACLhjzWdpFSDfs)lh ztSVx@_IF_yao?*`Jd**hqOO~S!vO7Ag;dtO-ki*u9o3QDK>Q|Ezv#pq90W<=rK#=G z9$rz=tnkpzbT<1jaI0qYsyQW~AX1kAx$3iDfwSf(2kr+x{RUPJp#h+kJ9DUni)0l1 zyi$?8zj4ITgv_h1-b~CjW7@?hB;*>GONI?+b>s+Pg+@e1&b&+{Rs1v`NVl>1(}V?- zeDumlSy?%{-W%AGw>&87T8^);uP5dku@hDg+?7HekI`OErBcz%2)picu-Dl5I8y7g z`~IYBwp^`78M9Gp3hziV9Gm9)o10fiI4yUrMv4`bM5|AE+}qr=E3MYnPu$X^=3BL7x)PaS_Mw2#zy}RrT4B3M{sG*gc(4Ef literal 0 HcmV?d00001 From d2c2ee95a68ca73fa10e966acdc0edc49c20d2db Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 7 Apr 2025 10:55:27 -0500 Subject: [PATCH 008/116] Set under/over indices for diffs in sea ice conc/thick --- mpas_analysis/default.cfg | 8 ++++---- mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py | 4 ++-- mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mpas_analysis/default.cfg b/mpas_analysis/default.cfg index 3fcd11d33..1d6d55ca5 100755 --- a/mpas_analysis/default.cfg +++ b/mpas_analysis/default.cfg @@ -4502,7 +4502,7 @@ colormapNameDifference = balance # whether the colormap is indexed or continuous colormapTypeDifference = indexed # color indices into colormapName for filled contours -colormapIndicesDifference = [0, 32, 64, 96, 112, 128, 128, 144, 160, 192, 224, 255] +colormapIndicesDifference = [0, 0, 26, 51, 77, 102, 128, 128, 153, 179, 204, 230, 255, 255] # colormap levels/values for contour boundaries colorbarLevelsDifference = [-0.5, -0.4, -0.3, -0.2, -0.1, -0.05, 0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5] @@ -4548,7 +4548,7 @@ colormapNameDifference = balance # whether the colormap is indexed or continuous colormapTypeDifference = indexed # color indices into colormapName for filled contours -colormapIndicesDifference = [0, 32, 64, 96, 112, 128, 128, 144, 160, 192, 224, 255] +colormapIndicesDifference = [0, 0, 26, 51, 77, 102, 128, 128, 153, 179, 204, 230, 255, 255] # colormap levels/values for contour boundaries colorbarLevelsDifference = [-0.5, -0.4, -0.3, -0.2, -0.1, -0.05, 0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5] @@ -4594,7 +4594,7 @@ colormapNameDifference = balance # whether the colormap is indexed or continuous colormapTypeDifference = indexed # color indices into colormapName for filled contours -colormapIndicesDifference = [0, 32, 64, 96, 128, 128, 160, 192, 224, 255] +colormapIndicesDifference = [0, 0, 32, 64, 96, 128, 128, 160, 192, 223, 255, 255] # colormap levels/values for contour boundaries colorbarLevelsDifference = [-3., -2.5, -2, -0.5, -0.1, 0, 0.1, 0.5, 2, 2.5, 3.] @@ -4635,7 +4635,7 @@ colormapNameDifference = balance # whether the colormap is indexed or continuous colormapTypeDifference = indexed # color indices into colormapName for filled contours -colormapIndicesDifference = [0, 32, 64, 96, 128, 128, 160, 192, 224, 255] +colormapIndicesDifference = [0, 0, 32, 64, 96, 128, 128, 160, 192, 223, 255, 255] # colormap levels/values for contour boundaries colorbarLevelsDifference = [-3., -2.5, -2, -0.5, -0.1, 0, 0.1, 0.5, 2, 2.5, 3.] diff --git a/mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py b/mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py index 974cf6c58..94911b02e 100644 --- a/mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py +++ b/mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py @@ -184,7 +184,7 @@ def _add_obs_tasks(self, seasons, comparisonGridNames, hemisphere, galleryName='Observations: SSM/I {}'.format( prefix), maskMinThreshold=minConcentration, - extend='neither', + extend='both', prependComparisonGrid=False) self.add_subtask(subtask) @@ -231,7 +231,7 @@ def _add_ref_tasks(self, seasons, comparisonGridNames, hemisphere, groupLink='{}_conc'.format(hemisphere.lower()), galleryName=galleryName, maskMinThreshold=minConcentration, - extend='neither', + extend='both', prependComparisonGrid=False) self.add_subtask(subtask) diff --git a/mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py b/mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py index 752c682c3..851ee4a7b 100644 --- a/mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py +++ b/mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py @@ -166,7 +166,7 @@ def __init__(self, config, mpasClimatologyTask, hemisphere, groupLink=f'{hemisphere.lower()}_thick', galleryName=galleryName, maskMinThreshold=0, - extend='neither', + extend='both', prependComparisonGrid=False) self.add_subtask(subtask) From d61ed9b075bbb2df9f4f492cf323e7fcbc88b273 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 7 Apr 2025 12:39:38 -0500 Subject: [PATCH 009/116] Update docs --- .../tasks/climatologyMapSeaIceConcNH.rst | 23 ++++++++++--------- .../tasks/climatologyMapSeaIceConcSH.rst | 23 ++++++++++--------- .../tasks/climatologyMapSeaIceThickNH.rst | 18 +++++++-------- .../tasks/climatologyMapSeaIceThickSH.rst | 18 +++++++-------- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/docs/users_guide/tasks/climatologyMapSeaIceConcNH.rst b/docs/users_guide/tasks/climatologyMapSeaIceConcNH.rst index d4516f68d..40b160a39 100644 --- a/docs/users_guide/tasks/climatologyMapSeaIceConcNH.rst +++ b/docs/users_guide/tasks/climatologyMapSeaIceConcNH.rst @@ -18,11 +18,13 @@ The following configuration options are available for this task:: [climatologyMapSeaIceConcNH] ## options related to plotting horizontally remapped climatologies of - ## sea ice concentration against reference model results and observations + ## sea ice concentration against control model results and observations ## in the northern hemisphere (NH) # colormap for model/observations colormapNameResult = ice + # whether the colormap is indexed or continuous + colormapTypeResult = indexed # color indices into colormapName for filled contours colormapIndicesResult = [20, 80, 110, 140, 170, 200, 230, 255] # colormap levels/values for contour boundaries @@ -30,23 +32,19 @@ The following configuration options are available for this task:: # colormap for differences colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = indexed # color indices into colormapName for filled contours - colormapIndicesDifference = [0, 32, 64, 96, 112, 128, 128, 144, 160, 192, - 224, 255] + colormapIndicesDifference = [0, 0, 26, 51, 77, 102, 128, 128, 153, 179, 204, 230, 255, 255] # colormap levels/values for contour boundaries - colorbarLevelsDifference = [-1., -0.8, -0.6, -0.4, -0.2, -0.1, 0, 0.1, 0.2, - 0.4, 0.6, 0.8, 1.] + colorbarLevelsDifference = [-0.5, -0.4, -0.3, -0.2, -0.1, -0.05, 0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5] # Months or seasons to plot (These should be left unchanged, since # observations are only available for these seasons) seasons = ['JFM', 'JAS'] - # comparison grid(s) ('latlon', 'antarctic') on which to plot analysis - comparisonGrids = ['latlon'] - - # reference lat/lon for sea ice plots in the northern hemisphere - minimumLatitude = 50 - referenceLongitude = 0 + # comparison grid(s) (typically 'arctic_extended') on which to plot analysis + comparisonGrids = ['arctic_extended'] # a list of prefixes describing the sources of the observations to be used observationPrefixes = ['NASATeam', 'Bootstrap'] @@ -54,6 +52,9 @@ The following configuration options are available for this task:: # arrange subplots vertically? vertical = False + # the minimum threshold below which concentration is masked out + minConcentration = 0.15 + # observations files concentrationNASATeamNH_JFM = SSMI/NASATeam_NSIDC0051/SSMI_NASATeam_gridded_concentration_NH_jfm.interp0.5x0.5_20180710.nc concentrationNASATeamNH_JAS = SSMI/NASATeam_NSIDC0051/SSMI_NASATeam_gridded_concentration_NH_jas.interp0.5x0.5_20180710.nc diff --git a/docs/users_guide/tasks/climatologyMapSeaIceConcSH.rst b/docs/users_guide/tasks/climatologyMapSeaIceConcSH.rst index 4ca3219a8..844bc2abf 100644 --- a/docs/users_guide/tasks/climatologyMapSeaIceConcSH.rst +++ b/docs/users_guide/tasks/climatologyMapSeaIceConcSH.rst @@ -18,11 +18,13 @@ The following configuration options are available for this task:: [climatologyMapSeaIceConcSH] ## options related to plotting horizontally remapped climatologies of - ## sea ice concentration against reference model results and observations + ## sea ice concentration against control model results and observations ## in the southern hemisphere (SH) # colormap for model/observations colormapNameResult = ice + # whether the colormap is indexed or continuous + colormapTypeResult = indexed # color indices into colormapName for filled contours colormapIndicesResult = [20, 80, 110, 140, 170, 200, 230, 255] # colormap levels/values for contour boundaries @@ -30,23 +32,19 @@ The following configuration options are available for this task:: # colormap for differences colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = indexed # color indices into colormapName for filled contours - colormapIndicesDifference = [0, 32, 64, 96, 112, 128, 128, 144, 160, 192, - 224, 255] + colormapIndicesDifference = [0, 0, 26, 51, 77, 102, 128, 128, 153, 179, 204, 230, 255, 255] # colormap levels/values for contour boundaries - colorbarLevelsDifference = [-1., -0.8, -0.6, -0.4, -0.2, -0.1, 0, 0.1, 0.2, - 0.4, 0.6, 0.8, 1.] + colorbarLevelsDifference = [-0.5, -0.4, -0.3, -0.2, -0.1, -0.05, 0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5] # Months or seasons to plot (These should be left unchanged, since # observations are only available for these seasons) seasons = ['DJF', 'JJA'] - # comparison grid(s) ('latlon', 'antarctic') on which to plot analysis - comparisonGrids = ['latlon'] - - # reference lat/lon for sea ice plots in the northern hemisphere - minimumLatitude = -50 - referenceLongitude = 180 + # comparison grid(s) (typically 'antarctic_extended') on which to plot analysis + comparisonGrids = ['antarctic_extended'] # a list of prefixes describing the sources of the observations to be used observationPrefixes = ['NASATeam', 'Bootstrap'] @@ -54,6 +52,9 @@ The following configuration options are available for this task:: # arrange subplots vertically? vertical = False + # the minimum threshold below which concentration is masked out + minConcentration = 0.15 + # observations files concentrationNASATeamSH_DJF = SSMI/NASATeam_NSIDC0051/SSMI_NASATeam_gridded_concentration_SH_djf.interp0.5x0.5_20180710.nc concentrationNASATeamSH_JJA = SSMI/NASATeam_NSIDC0051/SSMI_NASATeam_gridded_concentration_SH_jja.interp0.5x0.5_20180710.nc diff --git a/docs/users_guide/tasks/climatologyMapSeaIceThickNH.rst b/docs/users_guide/tasks/climatologyMapSeaIceThickNH.rst index dd01c247f..eb74cd057 100644 --- a/docs/users_guide/tasks/climatologyMapSeaIceThickNH.rst +++ b/docs/users_guide/tasks/climatologyMapSeaIceThickNH.rst @@ -19,11 +19,13 @@ The following configuration options are available for this task:: [climatologyMapSeaIceThickNH] ## options related to plotting horizontally remapped climatologies of - ## sea ice thickness against reference model results and observations + ## sea ice thickness against control model results and observations ## in the northern hemisphere (NH) # colormap for model/observations - colormapNameResult = ice + colormapNameResult = davos + # whether the colormap is indexed or continuous + colormapTypeResult = indexed # color indices into colormapName for filled contours colormapIndicesResult = [20, 80, 110, 140, 170, 200, 230, 255] # colormap levels/values for contour boundaries @@ -31,8 +33,10 @@ The following configuration options are available for this task:: # colormap for differences colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = indexed # color indices into colormapName for filled contours - colormapIndicesDifference = [0, 32, 64, 96, 128, 128, 160, 192, 224, 255] + colormapIndicesDifference = [0, 0, 32, 64, 96, 128, 128, 160, 192, 223, 255, 255] # colormap levels/values for contour boundaries colorbarLevelsDifference = [-3., -2.5, -2, -0.5, -0.1, 0, 0.1, 0.5, 2, 2.5, 3.] @@ -40,12 +44,8 @@ The following configuration options are available for this task:: # observations are only available for these seasons) seasons = ['FM', 'ON'] - # comparison grid(s) ('latlon', 'antarctic') on which to plot analysis - comparisonGrids = ['latlon'] - - # reference lat/lon for sea ice plots in the northern hemisphere - minimumLatitude = 50 - referenceLongitude = 0 + # comparison grid(s) (typically 'arctic_extended') on which to plot analysis + comparisonGrids = ['arctic_extended'] # a list of prefixes describing the sources of the observations to be used observationPrefixes = [''] diff --git a/docs/users_guide/tasks/climatologyMapSeaIceThickSH.rst b/docs/users_guide/tasks/climatologyMapSeaIceThickSH.rst index 883faeb5b..486e6742a 100644 --- a/docs/users_guide/tasks/climatologyMapSeaIceThickSH.rst +++ b/docs/users_guide/tasks/climatologyMapSeaIceThickSH.rst @@ -19,11 +19,13 @@ The following configuration options are available for this task:: [climatologyMapSeaIceThickSH] ## options related to plotting horizontally remapped climatologies of - ## sea ice thickness against reference model results and observations + ## sea ice thickness against control model results and observations ## in the southern hemisphere (SH) # colormap for model/observations - colormapNameResult = ice + colormapNameResult = davos + # whether the colormap is indexed or continuous + colormapTypeResult = indexed # color indices into colormapName for filled contours colormapIndicesResult = [20, 80, 110, 140, 170, 200, 230, 255] # colormap levels/values for contour boundaries @@ -31,8 +33,10 @@ The following configuration options are available for this task:: # colormap for differences colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = indexed # color indices into colormapName for filled contours - colormapIndicesDifference = [0, 32, 64, 96, 128, 128, 160, 192, 224, 255] + colormapIndicesDifference = [0, 0, 32, 64, 96, 128, 128, 160, 192, 223, 255, 255] # colormap levels/values for contour boundaries colorbarLevelsDifference = [-3., -2.5, -2, -0.5, -0.1, 0, 0.1, 0.5, 2, 2.5, 3.] @@ -40,12 +44,8 @@ The following configuration options are available for this task:: # observations are only available for these seasons) seasons = ['FM', 'ON'] - # comparison grid(s) ('latlon', 'antarctic') on which to plot analysis - comparisonGrids = ['latlon'] - - # reference lat/lon for sea ice plots in the northern hemisphere - minimumLatitude = -50 - referenceLongitude = 180 + # comparison grid(s) (typically 'antarctic_extended') on which to plot analysis + comparisonGrids = ['antarctic_extended'] # a list of prefixes describing the sources of the observations to be used observationPrefixes = [''] From a5c9994ca315296252b1e5458e9f4f9e0b5f2964 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 25 Mar 2025 22:32:29 -0500 Subject: [PATCH 010/116] Check that all BSF projections are present If not, delete climatology and start fresh --- mpas_analysis/ocean/climatology_map_bsf.py | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/mpas_analysis/ocean/climatology_map_bsf.py b/mpas_analysis/ocean/climatology_map_bsf.py index c30a6f119..ddae4ade9 100644 --- a/mpas_analysis/ocean/climatology_map_bsf.py +++ b/mpas_analysis/ocean/climatology_map_bsf.py @@ -8,6 +8,8 @@ # Additional copyright and license information can be found in the LICENSE file # distributed with this code, or at # https://raw.githubusercontent.com/MPAS-Dev/MPAS-Analysis/main/LICENSE +import os + import xarray as xr import numpy as np import scipy.sparse @@ -253,6 +255,39 @@ def setup_and_check(self): # Add the variables and seasons, now that we have the variable list self.mpasClimatologyTask.add_variables(self.variableList, self.seasons) + def run_task(self): + """ + Compute the requested climatologies + """ + config = self.config + # check if climatology exists and if all comparison grids are present + for season in self.seasons: + masked_climatology_filename = self.get_masked_file_name(season) + if not os.path.exists(masked_climatology_filename): + continue + all_found = True + with xr.open_dataset(masked_climatology_filename) as ds: + for comparison_grid_name in self.comparisonDescriptors.keys(): + grid_suffix = \ + comparison_grid_option_suffixes[comparison_grid_name] + config_section_name = f'{self.taskName}{grid_suffix}' + if config.has_section(config_section_name): + mpas_field_name = \ + f'barotropicStreamfunction{grid_suffix}' + if mpas_field_name not in ds: + all_found = False + break + if not all_found: + # if not, remove the files and recompute/remap + os.remove(masked_climatology_filename) + for comparison_grid_name in self.comparisonDescriptors.keys(): + remapped_filename = self.get_remapped_file_name( + season, comparison_grid_name) + if os.path.exists(remapped_filename): + os.remove(remapped_filename) + + super().run_task() + def customize_masked_climatology(self, climatology, season): """ Compute the masked climatology from the normal velocity From 4b7cb9b67068ade892160b99daf2638c9ee82352 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 7 Apr 2025 14:17:29 -0500 Subject: [PATCH 011/116] Fix conservation when list of inputs is empty When recomputing the conservation analaysis (e.g. in a main vs. control run or if analysis successfully ran previously), we can end up with an empty list of remaining inputs to process. In this case, we should just move on rather than trying to print the empty list. --- mpas_analysis/ocean/conservation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mpas_analysis/ocean/conservation.py b/mpas_analysis/ocean/conservation.py index 6ae6f0f28..ecd251edf 100644 --- a/mpas_analysis/ocean/conservation.py +++ b/mpas_analysis/ocean/conservation.py @@ -743,6 +743,10 @@ def _compute_time_series_with_xarray(self, variable_list): append, inputFiles = self._check_output_safe_to_append(variable_list) + if len(inputFiles) == 0: + # nothing to do + return + # Open all input files as a single dataset self.logger.info( f'Opening input files with xarray: {inputFiles[0]} ... ' From f0cab9aa902c2a45ad04c02f9a11f493a276048d Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 15 Feb 2025 15:03:03 +0100 Subject: [PATCH 012/116] Update to mpas_tools >=1.0.0 and pyremap >=2.0.0 --- ci/recipe/meta.yaml | 4 ++-- dev-spec.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ci/recipe/meta.yaml b/ci/recipe/meta.yaml index 570f56586..bfd22bb0d 100644 --- a/ci/recipe/meta.yaml +++ b/ci/recipe/meta.yaml @@ -32,7 +32,7 @@ requirements: - lxml - mache >=1.11.0 - matplotlib-base >=3.9.0 - - mpas_tools >=0.34.1,<1.0.0 + - mpas_tools >=1.0.0,<2.0.0 - nco >=4.8.1,!=5.2.6 - netcdf4 - numpy >=2.0,<3.0 @@ -40,7 +40,7 @@ requirements: - pillow >=10.0.0,<11.0.0 - progressbar2 - pyproj - - pyremap >=1.2.0,<2.0.0 + - pyremap >=2.0.0,<3.0.0 - python-dateutil - requests - scipy >=1.7.0 diff --git a/dev-spec.txt b/dev-spec.txt index ca9a1f888..9f826e7bb 100644 --- a/dev-spec.txt +++ b/dev-spec.txt @@ -15,7 +15,7 @@ gsw lxml mache >=1.11.0 matplotlib-base>=3.9.0 -mpas_tools>=0.34.1,<1.0.0 +mpas_tools >=1.0.0,<2.0.0 nco>=4.8.1,!=5.2.6 netcdf4 numpy>=2.0,<3.0 @@ -23,7 +23,7 @@ pandas pillow >=10.0.0,<11.0.0 progressbar2 pyproj -pyremap>=1.2.0,<2.0.0 +pyremap >=2.0.0,<3.0.0 python-dateutil requests scipy >=1.7.0 From e01c41d70d55900b8a48442dc12599296169137a Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 15 Feb 2025 08:07:49 -0600 Subject: [PATCH 013/116] Switch mpas_dev to mpas_analysis_dev environment name --- README.md | 10 +++--- configs/alcf/job_script.cooley.bash | 2 +- configs/compy/job_script.compy.bash | 2 +- configs/job_script.default.bash | 2 +- configs/lanl/job_script.lanl.bash | 2 +- configs/lcrc/job_script.anvil.bash | 2 +- configs/lcrc/job_script.chrysalis.bash | 2 +- configs/nersc/job_script.cori-haswell.bash | 2 +- configs/nersc/job_script.cori-knl.bash | 2 +- configs/nersc/job_script.pm-cpu.bash | 2 +- configs/olcf/job_script.olcf.bash | 2 +- docs/tutorials/dev_add_task.rst | 10 +++--- docs/tutorials/dev_getting_started.rst | 36 +++++++++++----------- suite/run_dev_suite.bash | 2 +- 14 files changed, 39 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 7c7ce60de..94a6ea76d 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ environment): ``` bash conda config --add channels conda-forge conda config --set channel_priority strict -conda create -y -n mpas_dev --file dev-spec.txt -conda activate mpas_dev +conda create -y -n mpas_analysis_dev --file dev-spec.txt +conda activate mpas_analysis_dev python -m pip install --no-deps --no-build-isolation -e . ``` @@ -64,16 +64,16 @@ for MPAS-Tools or geometric\_features), you should first comment out the other package in `dev-spec.txt`. Then, you can install both packages in the same development environment, e.g.: ``` bash -conda create -y -n mpas_dev --file tools/MPAS-Tools/conda_package/dev-spec.txt \ +conda create -y -n mpas_analysis_dev --file tools/MPAS-Tools/conda_package/dev-spec.txt \ --file analysis/MPAS-Analysis/dev-spec.txt -conda activate mpas_dev +conda activate mpas_analysis_dev cd tools/MPAS-Tools/conda_package python -m pip install --no-deps --no-build-isolation -e . cd ../../../analysis/MPAS-Analysis python -m pip install --no-deps --no-build-isolation -e . ``` Obviously, the paths to the repos may be different in your local clones. With -the `mpas_dev` environment as defined above, you can make changes to both +the `mpas_analysis_dev` environment as defined above, you can make changes to both `mpas_tools` and `mpas-analysis` packages in their respective branches, and these changes will be reflected when refer to the packages or call their respective entry points (command-line tools). diff --git a/configs/alcf/job_script.cooley.bash b/configs/alcf/job_script.cooley.bash index c5eda7b05..8a2ea6614 100755 --- a/configs/alcf/job_script.cooley.bash +++ b/configs/alcf/job_script.cooley.bash @@ -6,7 +6,7 @@ source /lus/theta-fs0/projects/ccsm/acme/tools/e3sm-unified/load_latest_e3sm_unified_cooley.sh # alternatively, you can load your own development environment # source ~/mambaforge/etc/profile.d/conda.sh -# conda activate mpas_dev +# conda activate mpas_analysis_dev # export E3SMU_MACHINE=cooley export HDF5_USE_FILE_LOCKING=FALSE diff --git a/configs/compy/job_script.compy.bash b/configs/compy/job_script.compy.bash index 33b153c68..916a6f63f 100644 --- a/configs/compy/job_script.compy.bash +++ b/configs/compy/job_script.compy.bash @@ -11,7 +11,7 @@ export OMP_NUM_THREADS=1 source /share/apps/E3SM/conda_envs/load_latest_e3sm_unified_compy.sh # alternatively, you can load your own development environment # source ~/mambaforge/etc/profile.d/conda.sh -# conda activate mpas_dev +# conda activate mpas_analysis_dev # export E3SMU_MACHINE=compy export HDF5_USE_FILE_LOCKING=FALSE diff --git a/configs/job_script.default.bash b/configs/job_script.default.bash index 29871522c..e1d61118f 100755 --- a/configs/job_script.default.bash +++ b/configs/job_script.default.bash @@ -8,7 +8,7 @@ export OMP_NUM_THREADS=1 source ~/mambaforge/etc/profile.d/conda.sh -conda activate mpas_dev +conda activate mpas_analysis_dev # if you are on an E3SM supported machine, you can specify it: # export E3SMU_MACHINE=chrysalis diff --git a/configs/lanl/job_script.lanl.bash b/configs/lanl/job_script.lanl.bash index 36b861410..b32399a07 100644 --- a/configs/lanl/job_script.lanl.bash +++ b/configs/lanl/job_script.lanl.bash @@ -8,7 +8,7 @@ source /users/xylar/climate/mambaforge/etc/profile.d/conda.sh source /users/xylar/climate/mambaforge/etc/profile.d/mamba.sh -mamba activate mpas_dev +mamba activate mpas_analysis_dev export HDF5_USE_FILE_LOCKING=FALSE diff --git a/configs/lcrc/job_script.anvil.bash b/configs/lcrc/job_script.anvil.bash index ef20fa541..8765a2f4a 100644 --- a/configs/lcrc/job_script.anvil.bash +++ b/configs/lcrc/job_script.anvil.bash @@ -12,7 +12,7 @@ export OMP_NUM_THREADS=1 source /lcrc/soft/climate/e3sm-unified/load_latest_e3sm_unified_anvil.sh # alternatively, you can load your own development environment # source ~/mambaforge/etc/profile.d/conda.sh -# conda activate mpas_dev +# conda activate mpas_analysis_dev # export E3SMU_MACHINE=anvil export HDF5_USE_FILE_LOCKING=FALSE diff --git a/configs/lcrc/job_script.chrysalis.bash b/configs/lcrc/job_script.chrysalis.bash index 598115df5..ab2882b78 100644 --- a/configs/lcrc/job_script.chrysalis.bash +++ b/configs/lcrc/job_script.chrysalis.bash @@ -10,7 +10,7 @@ export OMP_NUM_THREADS=1 source /lcrc/soft/climate/e3sm-unified/load_latest_e3sm_unified_chrysalis.sh # alternatively, you can load your own development environment # source ~/mambaforge/etc/profile.d/conda.sh -# conda activate mpas_dev +# conda activate mpas_analysis_dev # export E3SMU_MACHINE=chrysalis export HDF5_USE_FILE_LOCKING=FALSE diff --git a/configs/nersc/job_script.cori-haswell.bash b/configs/nersc/job_script.cori-haswell.bash index 75140ba57..210681e40 100644 --- a/configs/nersc/job_script.cori-haswell.bash +++ b/configs/nersc/job_script.cori-haswell.bash @@ -20,7 +20,7 @@ export OMP_NUM_THREADS=1 source /global/common/software/e3sm/anaconda_envs/load_latest_e3sm_unified_cori-haswell.sh # alternatively, you can load your own development environment # source ~/mambaforge/etc/profile.d/conda.sh -# conda activate mpas_dev +# conda activate mpas_analysis_dev # export E3SMU_MACHINE=cori-haswell export HDF5_USE_FILE_LOCKING=FALSE diff --git a/configs/nersc/job_script.cori-knl.bash b/configs/nersc/job_script.cori-knl.bash index 679157bb1..2eb57758d 100644 --- a/configs/nersc/job_script.cori-knl.bash +++ b/configs/nersc/job_script.cori-knl.bash @@ -20,7 +20,7 @@ export OMP_NUM_THREADS=1 source /global/common/software/e3sm/anaconda_envs/load_latest_e3sm_unified_cori-knl.sh # alternatively, you can load your own development environment # source ~/mambaforge/etc/profile.d/conda.sh -# conda activate mpas_dev +# conda activate mpas_analysis_dev # export E3SMU_MACHINE=cori-knl export HDF5_USE_FILE_LOCKING=FALSE diff --git a/configs/nersc/job_script.pm-cpu.bash b/configs/nersc/job_script.pm-cpu.bash index f096ca4b0..d4abe4969 100644 --- a/configs/nersc/job_script.pm-cpu.bash +++ b/configs/nersc/job_script.pm-cpu.bash @@ -14,7 +14,7 @@ export OMP_NUM_THREADS=1 source /global/common/software/e3sm/anaconda_envs/load_latest_e3sm_unified_pm-cpu.sh # alternatively, you can load your own development environment # source ~/mambaforge/etc/profile.d/conda.sh -# conda activate mpas_dev +# conda activate mpas_analysis_dev # export E3SMU_MACHINE=pm-cpu export HDF5_USE_FILE_LOCKING=FALSE diff --git a/configs/olcf/job_script.olcf.bash b/configs/olcf/job_script.olcf.bash index 431d1bebc..f777f74a4 100644 --- a/configs/olcf/job_script.olcf.bash +++ b/configs/olcf/job_script.olcf.bash @@ -10,7 +10,7 @@ source /gpfs/alpine/proj-shared/cli115/e3sm-unified/load_latest_e3sm_unified_andes.csh # alternatively, you can load your own development environment # source ~/mambaforge/etc/profile.d/conda.sh -# conda activate mpas_dev +# conda activate mpas_analysis_dev # export E3SMU_MACHINE=anvil export HDF5_USE_FILE_LOCKING=FALSE diff --git a/docs/tutorials/dev_add_task.rst b/docs/tutorials/dev_add_task.rst index 0282cc641..5b7bc5072 100644 --- a/docs/tutorials/dev_add_task.rst +++ b/docs/tutorials/dev_add_task.rst @@ -34,7 +34,7 @@ the code to MPAS-Analysis. If one just wishes to add a new field that already exists in MPAS-Ocean or MPAS-Seaice output, only a few of the steps below are necessary: - 1. Follow step 1 to set up an ```mpas_dev``` environment. + 1. Follow step 1 to set up an ```mpas_analysis_dev``` environment. 2. Copy an existing `ocean `_ or `sea_ice `_ python module to a new name and edit it as needed for the new fields. @@ -58,7 +58,7 @@ testing your new MPAS-Analysis development, and running MPAS-Analysis. Make sure you follow the tutorial for developers, not for users, since the tutorial for users installs the latest release of MPAS-Analysis, which you cannot modify. Similarly, changes must be tested in your own development - environment (often called ``mpas_dev``) rather than the in a shared + environment (often called ``mpas_analysis_dev``) rather than the in a shared environment like `E3SM-Unified `_. Then, please follow the :ref:`tutorial_understand_a_task`. This will give @@ -550,12 +550,12 @@ whatever editor you like.) code . -I'll create or recreate my ``mpas_dev`` environment as in +I'll create or recreate my ``mpas_analysis_dev`` environment as in :ref:`tutorial_dev_getting_started`, and then make sure to at least do: .. code-block:: bash - conda activate mpas_dev + conda activate mpas_analysis_dev python -m pip install --no-deps --no-build-isolation -e . This last command installs the ``mpas_analysis`` package into the conda @@ -1138,7 +1138,7 @@ You also need to add the tasks class and public methods to the in the developer's guide. Again, the easiest approach is to copy the section for a similar task and modify as needed. -With the ``mpas_dev`` environment activated, you can run: +With the ``mpas_analysis_dev`` environment activated, you can run: .. code-block:: bash diff --git a/docs/tutorials/dev_getting_started.rst b/docs/tutorials/dev_getting_started.rst index 8ff064578..c7ad2821e 100644 --- a/docs/tutorials/dev_getting_started.rst +++ b/docs/tutorials/dev_getting_started.rst @@ -249,13 +249,13 @@ If you installed Miniforge3, these steps will happen automatically. 4.3 Create a development environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can create a new conda environment called ``mpas_dev`` and install the +You can create a new conda environment called ``mpas_analysis_dev`` and install the dependencies that MPAS-Analysis needs by running the following in the worktree where you are doing your development: .. code-block:: bash - $ conda create -y -n mpas_dev --file dev-spec.txt + $ conda create -y -n mpas_analysis_dev --file dev-spec.txt The last argument is only needed on HPC machines because the conda version of MPI doesn't work properly on these machines. You can omit it if you're @@ -266,21 +266,21 @@ mode by running: .. code-block:: bash - $ conda activate mpas_dev + $ conda activate mpas_analysis_dev $ python -m pip install --no-deps --no-build-isolation -e . In this mode, any edits you make to the code in the worktree will be available in the conda environment. If you run ``mpas_analysis`` on the command line, it will know about the changes. -This command only needs to be done once after the ``mpas_dev`` environment is +This command only needs to be done once after the ``mpas_analysis_dev`` environment is built if you are not using worktrees. .. note:: If you do use worktrees, rerun the ``python -m pip install ...`` command each time you switch to developing a new branch, since otherwise the - version of ``mpas_analysis`` in the ``mpas_dev`` environment will be the + version of ``mpas_analysis`` in the ``mpas_analysis_dev`` environment will be the one you were developing previously. .. _tutorial_dev_get_started_activ_env: @@ -288,20 +288,20 @@ built if you are not using worktrees. 4.4 Activating the environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Each time you open a new terminal window, to activate the ``mpas_dev`` +Each time you open a new terminal window, to activate the ``mpas_analysis_dev`` environment, you will need to run either for ``bash``: .. code-block:: bash $ source ~/miniforge3/etc/profile.d/conda.sh - $ conda activate mpas_dev + $ conda activate mpas_analysis_dev or for ``csh``: .. code-block:: csh > source ~/miniforge3/etc/profile.d/conda.csh - > conda activate mpas_dev + > conda activate mpas_analysis_dev You can skip the ``source`` command if you chose to initialize Miniforge3 or Miniconda3 so it loads automatically. You can also use the ``init_conda`` @@ -311,8 +311,8 @@ alias for this step if you defined one. ~~~~~~~~~~~~~~~~~~~~~~~ If you switch to a different worktree, it is safest to rerun the whole -process for creating the ``mpas_dev`` conda environment. If you know that -the dependencies are the same as the worktree used to create ``mpas_dev``, +process for creating the ``mpas_analysis_dev`` conda environment. If you know that +the dependencies are the same as the worktree used to create ``mpas_analysis_dev``, You can just reinstall ``mpas_analysis`` itself by rerunning .. code-block:: bash @@ -320,7 +320,7 @@ You can just reinstall ``mpas_analysis`` itself by rerunning python -m pip install --no-deps --no-build-isolation -e . in the new worktree. If you forget this step, you will find that changes you -make in the worktree don't affect the ``mpas_dev`` conda environment you are +make in the worktree don't affect the ``mpas_analysis_dev`` conda environment you are using. 5. Editing code @@ -348,7 +348,7 @@ need to follow steps 2-6 of the :ref:`tutorial_getting_started` tutorial. Run ``mpas_analysis`` on a compute node, not on an HPC login nodes (front ends), because it uses too many resources to be safely run on a login node. - When using a compute node interactively, activate the ``mpas_dev`` + When using a compute node interactively, activate the ``mpas_analysis_dev`` environment, even if it was activated on the login node. Be sure to 7.1 Configuring MPAS-Analysis @@ -688,7 +688,7 @@ also be displayed over the full 5 years.) The hard work is done. Now that we have a config file, we are ready to run. To run MPAS-Analysis, you should either create a job script or log into -an interactive session on a compute node. Then, activate the ``mpas_dev`` +an interactive session on a compute node. Then, activate the ``mpas_analysis_dev`` conda environment as in :ref:`tutorial_dev_get_started_activ_env`. On many file systems, MPAS-Analysis and other python-based software that used @@ -724,15 +724,15 @@ Typical output is the analysis is running correctly looks something like: Detected E3SM supported machine: anvil Using the following config files: /gpfs/fs1/home/ac.xylar/code/mpas-analysis/add_my_fancy_task/mpas_analysis/default.cfg - /gpfs/fs1/home/ac.xylar/anvil/mambaforge/envs/mpas_dev/lib/python3.10/site-packages/mache/machines/anvil.cfg + /gpfs/fs1/home/ac.xylar/anvil/mambaforge/envs/mpas_analysis_dev/lib/python3.10/site-packages/mache/machines/anvil.cfg /gpfs/fs1/home/ac.xylar/code/mpas-analysis/add_my_fancy_task/mpas_analysis/configuration/anvil.cfg /gpfs/fs1/home/ac.xylar/code/mpas-analysis/add_my_fancy_task/mpas_analysis/__main__.py /gpfs/fs1/home/ac.xylar/code/mpas-analysis/add_my_fancy_task/myrun.cfg copying /gpfs/fs1/home/ac.xylar/code/mpas-analysis/add_my_fancy_task/myrun.cfg to HTML dir. - running: /gpfs/fs1/home/ac.xylar/anvil/mambaforge/envs/mpas_dev/bin/ESMF_RegridWeightGen --source /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmp76l7of28/src_mesh.nc --destination /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmp76l7of28/dst_mesh.nc --weight /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/map_oQU480_to_0.5x0.5degree_bilinear.nc --method bilinear --netcdf4 --no_log --src_loc center --src_regional --ignore_unmapped - running: /gpfs/fs1/home/ac.xylar/anvil/mambaforge/envs/mpas_dev/bin/ESMF_RegridWeightGen --source /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmpj94wpf9y/src_mesh.nc --destination /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmpj94wpf9y/dst_mesh.nc --weight /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/map_oQU480_to_6000.0x6000.0km_10.0km_Antarctic_stereo_bilinear.nc --method bilinear --netcdf4 --no_log --src_loc center --src_regional --dst_regional --ignore_unmapped - running: /gpfs/fs1/home/ac.xylar/anvil/mambaforge/envs/mpas_dev/bin/ESMF_RegridWeightGen --source /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmp6zm13a0s/src_mesh.nc --destination /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmp6zm13a0s/dst_mesh.nc --weight /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/map_oQU480_to_WOCE_transects_5km_bilinear.nc --method bilinear --netcdf4 --no_log --src_loc center --src_regional --dst_regional --ignore_unmapped + running: /gpfs/fs1/home/ac.xylar/anvil/mambaforge/envs/mpas_analysis_dev/bin/ESMF_RegridWeightGen --source /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmp76l7of28/src_mesh.nc --destination /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmp76l7of28/dst_mesh.nc --weight /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/map_oQU480_to_0.5x0.5degree_bilinear.nc --method bilinear --netcdf4 --no_log --src_loc center --src_regional --ignore_unmapped + running: /gpfs/fs1/home/ac.xylar/anvil/mambaforge/envs/mpas_analysis_dev/bin/ESMF_RegridWeightGen --source /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmpj94wpf9y/src_mesh.nc --destination /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmpj94wpf9y/dst_mesh.nc --weight /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/map_oQU480_to_6000.0x6000.0km_10.0km_Antarctic_stereo_bilinear.nc --method bilinear --netcdf4 --no_log --src_loc center --src_regional --dst_regional --ignore_unmapped + running: /gpfs/fs1/home/ac.xylar/anvil/mambaforge/envs/mpas_analysis_dev/bin/ESMF_RegridWeightGen --source /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmp6zm13a0s/src_mesh.nc --destination /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmp6zm13a0s/dst_mesh.nc --weight /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/map_oQU480_to_WOCE_transects_5km_bilinear.nc --method bilinear --netcdf4 --no_log --src_loc center --src_regional --dst_regional --ignore_unmapped Preprocessing SOSE transect data... temperature salinity @@ -741,7 +741,7 @@ Typical output is the analysis is running correctly looks something like: meridionalVelocity velMag Done. - running: /gpfs/fs1/home/ac.xylar/anvil/mambaforge/envs/mpas_dev/bin/ESMF_RegridWeightGen --source /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmpe2a9yblb/src_mesh.nc --destination /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmpe2a9yblb/dst_mesh.nc --weight /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/map_oQU480_to_SOSE_transects_5km_bilinear.nc --method bilinear --netcdf4 --no_log --src_loc center --src_regional --dst_regional --ignore_unmapped + running: /gpfs/fs1/home/ac.xylar/anvil/mambaforge/envs/mpas_analysis_dev/bin/ESMF_RegridWeightGen --source /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmpe2a9yblb/src_mesh.nc --destination /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/tmpe2a9yblb/dst_mesh.nc --weight /lcrc/group/e3sm/ac.xylar/analysis/A_WCYCL1850.ne4_oQU480.anvil/clim_3-5_ts_1-5/mapping/map_oQU480_to_SOSE_transects_5km_bilinear.nc --method bilinear --netcdf4 --no_log --src_loc center --src_regional --dst_regional --ignore_unmapped Running tasks: 100% |##########################################| Time: 0:06:42 diff --git a/suite/run_dev_suite.bash b/suite/run_dev_suite.bash index 0dc955bb7..a6962bd82 100755 --- a/suite/run_dev_suite.bash +++ b/suite/run_dev_suite.bash @@ -2,7 +2,7 @@ set -e -env_name=mpas_dev +env_name=mpas_analysis_dev conda_base=$(dirname $(dirname $CONDA_EXE)) source $conda_base/etc/profile.d/conda.sh From 61b2c7778e4d7f8c7eaa9dd8e489890070cc7499 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 15 Feb 2025 15:01:57 +0100 Subject: [PATCH 014/116] Update transects to mpas_tools 1.0.0 --- .../ocean/compute_transects_subtask.py | 93 ++++-- mpas_analysis/ocean/plot_transect_subtask.py | 42 +-- mpas_analysis/shared/plot/vertical_section.py | 279 +++++------------- 3 files changed, 143 insertions(+), 271 deletions(-) diff --git a/mpas_analysis/ocean/compute_transects_subtask.py b/mpas_analysis/ocean/compute_transects_subtask.py index 1f00d3cf3..14cd4ff7b 100644 --- a/mpas_analysis/ocean/compute_transects_subtask.py +++ b/mpas_analysis/ocean/compute_transects_subtask.py @@ -18,11 +18,15 @@ from mpas_tools.viz import mesh_to_triangles from mpas_tools.transects import subdivide_great_circle, \ cartesian_to_great_circle_distance -from mpas_tools.viz.transects import find_transect_cells_and_weights, \ +from mpas_tools.viz.transect.horiz import ( + find_spherical_transect_cells_and_weights, make_triangle_tree -from mpas_tools.ocean.transects import find_transect_levels_and_weights, \ - interp_mpas_to_transect_triangle_nodes, \ - interp_transect_grid_to_transect_triangle_nodes +) +from mpas_tools.ocean.viz.transect.vert import ( + find_transect_levels_and_weights, + interp_mpas_to_transect_nodes, + interp_transect_grid_to_transect_nodes +) from mpas_analysis.shared.climatology import RemapMpasClimatologySubtask, \ get_climatology_op_directory @@ -161,8 +165,8 @@ def __init__(self, mpasClimatologyTask, parentTask, climatologyName, self.remap = self.obsDatasets.horizontalResolution != 'mpas' if self.obsDatasets.horizontalResolution == 'mpas' and \ self.verticalComparisonGridName != 'mpas': - raise ValueError('If the horizontal comparison grid is "mpas", the ' - 'vertical grid must also be "mpas".') + raise ValueError('If the horizontal comparison grid is "mpas", ' + 'the vertical grid must also be "mpas".') def setup_and_check(self): """ @@ -471,6 +475,14 @@ def _compute_mpas_transects(self, dsMesh): dsTris = mesh_to_triangles(dsMesh) + layerThickness = dsMesh.layerThickness + bottomDepth = dsMesh.bottomDepth + maxLevelCell = dsMesh.maxLevelCell - 1 + if 'minLevelCell' in dsMesh: + minLevelCell = dsMesh.minLevelCell - 1 + else: + minLevelCell = xr.zeros_like(maxLevelCell) + triangleTree = make_triangle_tree(dsTris) for transectName in transectNames: @@ -493,22 +505,30 @@ def _compute_mpas_transects(self, dsMesh): else: transectZ = None - dsMpasTransect = find_transect_cells_and_weights( - dsTransect.lon, dsTransect.lat, dsTris, dsMesh, - triangleTree, degrees=True) + dsMpasTransect = find_spherical_transect_cells_and_weights( + lon_transect=dsTransect.lon, + lat_transect=dsTransect.lat, + ds_tris=dsTris, + ds_mesh=dsMesh, + tree=triangleTree, + degrees=True) dsMpasTransect = find_transect_levels_and_weights( - dsMpasTransect, dsMesh.layerThickness, - dsMesh.bottomDepth, dsMesh.maxLevelCell - 1, - transectZ) + ds_horiz_transect=dsMpasTransect, + layer_thickness=layerThickness, + bottom_depth=bottomDepth, + min_level_cell=minLevelCell, + max_level_cell=maxLevelCell, + z_transect=transectZ) if 'landIceFraction' in dsMesh: interpCellIndices = dsMpasTransect.interpHorizCellIndices interpCellWeights = dsMpasTransect.interpHorizCellWeights landIceFraction = dsMesh.landIceFraction.isel( nCells=interpCellIndices) - landIceFraction = (landIceFraction * interpCellWeights).sum( - dim='nHorizWeights') + landIceFraction = ( + landIceFraction * interpCellWeights).sum( + dim='nHorizWeights') dsMpasTransect['landIceFraction'] = landIceFraction # use to_netcdf rather than write_netcdf_with_fill because @@ -517,9 +537,7 @@ def _compute_mpas_transects(self, dsMesh): dsMpasTransect.to_netcdf(transectInfoFileName) dsTransectOnMpas = xr.Dataset(dsMpasTransect) - dsTransectOnMpas['x'] = dsMpasTransect.dNode.isel( - nSegments=dsMpasTransect.segmentIndices, - nHorizBounds=dsMpasTransect.nodeHorizBoundsIndices) + dsTransectOnMpas['x'] = dsMpasTransect.dNode dsTransectOnMpas['z'] = dsMpasTransect.zTransectNode @@ -545,9 +563,11 @@ def _compute_mpas_transects(self, dsMesh): dims = dsMask[var].dims if 'nCells' in dims and 'nVertLevels' in dims: dsOnMpas[var] = \ - interp_mpas_to_transect_triangle_nodes( + interp_mpas_to_transect_nodes( dsMpasTransect, dsMask[var]) + dsOnMpas = self._transpose(dsOnMpas) + outFileName = self.get_remapped_file_name( season, comparisonGridName=transectName) dsOnMpas.to_netcdf(outFileName) @@ -558,13 +578,37 @@ def _interp_obs_to_mpas(self, da, dsMpasTransect, threshold=0.1): """ daMask = da.notnull() da = da.where(daMask, 0.) - da = interp_transect_grid_to_transect_triangle_nodes( - dsMpasTransect, da) - daMask = interp_transect_grid_to_transect_triangle_nodes( - dsMpasTransect, daMask) + da = interp_transect_grid_to_transect_nodes( + ds_transect=dsMpasTransect, + da=da) + daMask = interp_transect_grid_to_transect_nodes( + ds_transect=dsMpasTransect, + da=daMask) da = (da / daMask).where(daMask > threshold) return da + @staticmethod + def _transpose(dsOnMpas): + """ + Transpose the data set to have the expected dimension order + """ + dims = dsOnMpas.dims + dimsTransposed = ['nPoints', 'nz', + 'nSegments', 'nHalfLevels', + 'nHorizLevels', 'nVertLevels', + 'nHorizWeights', 'nVertWeights'] + + # drop any dimensions not in the dataset + dimsTransposed = [dim for dim in dimsTransposed if dim in + dims] + # add any other dimensions at the end + for dim in dims: + if dim not in dimsTransposed: + dimsTransposed.append(dim) + dsOnMpas = dsOnMpas.transpose(*dimsTransposed) + + return dsOnMpas + class TransectsObservations(object): """ @@ -611,8 +655,9 @@ def __init__(self, config, obsFileNames, horizontalResolution, observations for a transect horizontalResolution : str - 'obs' for the obs as they are, 'mpas' for the native MPAS mesh, or a - size in km if subdivision of the observational transect is desired. + 'obs' for the obs as they are, 'mpas' for the native MPAS mesh, or + a size in km if subdivision of the observational transect is + desired. transectCollectionName : str A name that describes the collection of transects (e.g. the name diff --git a/mpas_analysis/ocean/plot_transect_subtask.py b/mpas_analysis/ocean/plot_transect_subtask.py index dc3168f41..a144872d2 100644 --- a/mpas_analysis/ocean/plot_transect_subtask.py +++ b/mpas_analysis/ocean/plot_transect_subtask.py @@ -23,8 +23,6 @@ from geometric_features import FeatureCollection -from mpas_tools.ocean.transects import get_outline_segments - from mpas_analysis.shared.plot import plot_vertical_section_comparison, \ savefig, add_inset @@ -397,18 +395,12 @@ def _plot_transect(self, remappedModelClimatology, remappedRefClimatology): else: x = 1e-3*remappedModelClimatology.dNode - z = None + z = remappedModelClimatology.zTransectNode lon = remappedModelClimatology.lonNode lat = remappedModelClimatology.latNode remappedModelClimatology['dNode'] = x - # flatten the x, lon and lat arrays because this is what - # vertical_section is expecting - x = xr.DataArray(data=x.values.ravel(), dims=('nx',)) - lon = xr.DataArray(data=lon.values.ravel(), dims=('nx',)) - lat = xr.DataArray(data=lat.values.ravel(), dims=('nx',)) - # This will do strange things at the antemeridian but there's little # we can do about that. lon_pm180 = numpy.mod(lon + 180., 360.) - 180. @@ -437,12 +429,6 @@ def _plot_transect(self, remappedModelClimatology, remappedRefClimatology): modelOutput = remappedModelClimatology[self.mpasFieldName] - if remap: - triangulation_args = None - else: - triangulation_args = self._get_ds_triangulation( - remappedModelClimatology) - if remappedRefClimatology is None: refOutput = None bias = None @@ -578,12 +564,11 @@ def _plot_transect(self, remappedModelClimatology, remappedRefClimatology): configSectionName, xCoords=xs, zCoord=z, - triangulation_args=triangulation_args, colorbarLabel=self.unitsLabel, xlabels=xLabels, ylabel=yLabel, title=title, - modelTitle='{}'.format(mainRunName), + modelTitle=mainRunName, refTitle=self.refTitleLabel, diffTitle=self.diffTitleLabel, numUpperTicks=numUpperTicks, @@ -694,7 +679,7 @@ def _lat_greater_extent(self, lat, lon): maxes = [] last_idx = 0 - while(len(lon_r) > 0 and len(lon_l) > 0): + while len(lon_r) > 0 and len(lon_l) > 0: if lon_r[0] < lon_l[0]: mins.append(numpy.min(lon[last_idx:lon_r[0]])) last_idx = lon_r[0] @@ -741,7 +726,8 @@ def _strictly_monotonic(self, coord): # Greg Streletz, Xylar Asay-Davis coord_diff = numpy.diff(coord.values) - coord_diff = numpy.where(coord_diff > 180, coord_diff - 360, coord_diff) + coord_diff = numpy.where(coord_diff > 180, coord_diff - 360, + coord_diff) coord_diff = numpy.where(coord_diff < -180, coord_diff + 360, coord_diff) return numpy.all(coord_diff > 0) or numpy.all(coord_diff < 0) @@ -838,24 +824,6 @@ def _lat_fewest_direction_changes(self, lat, lon): else: return False - def _get_ds_triangulation(self, dsTransectTriangles): - """get matplotlib Triangulation from triangulation dataset""" - - nTransectTriangles = dsTransectTriangles.sizes['nTransectTriangles'] - dNode = dsTransectTriangles.dNode.isel( - nSegments=dsTransectTriangles.segmentIndices, - nHorizBounds=dsTransectTriangles.nodeHorizBoundsIndices) - x = dNode.values.ravel() - - zTransectNode = dsTransectTriangles.zTransectNode - y = zTransectNode.values.ravel() - - tris = numpy.arange(3 * nTransectTriangles).reshape( - (nTransectTriangles, 3)) - triangulation_args = dict(x=x, y=y, triangles=tris) - - return triangulation_args - @staticmethod def _get_contour_colormap(): # https://stackoverflow.com/a/18926541/7728169 diff --git a/mpas_analysis/shared/plot/vertical_section.py b/mpas_analysis/shared/plot/vertical_section.py index e7f417350..04b57b5b2 100644 --- a/mpas_analysis/shared/plot/vertical_section.py +++ b/mpas_analysis/shared/plot/vertical_section.py @@ -18,8 +18,6 @@ import matplotlib import matplotlib.pyplot as plt -from matplotlib.tri import Triangulation -from mpl_toolkits.axes_grid1 import make_axes_locatable import xarray as xr import numpy as np @@ -38,13 +36,6 @@ def plot_vertical_section_comparison( colorMapSectionName, xCoords=None, zCoord=None, - triangulation_args=None, - xOutlineModel=None, - zOutlineModel=None, - xOutlineRef=None, - zOutlineRef=None, - xOutlineDiff=None, - zOutlineDiff=None, colorbarLabel=None, xlabels=None, ylabel=None, @@ -118,28 +109,6 @@ def plot_vertical_section_comparison( zCoord : xarray.DataArray, optional The z coordinates for the model, ref and diff arrays - triangulation_args : dict, optional - A dict of arguments to create a matplotlib.tri.Triangulation of the - transect that does not rely on it being on a logically rectangular grid. - The arguments rather than the triangulation itself are passed because - multiple triangulations with different masks are needed internally and - there is not an obvious mechanism for copying an existing triangulation. - If this option is provided, ``xCoords`` is only used for tick marks if - more than one x axis is requested, and ``zCoord`` will be ignored. - - xOutlineModel, zOutlineModel : numpy.ndarray, optional - pairs of points defining line segments that are used to outline the - valid region of the mesh for the model panel if ``outlineValid = True`` - and ``triangulation_args`` is not ``None`` - - xOutlineRef, zOutlineRef : numpy.ndarray, optional - Same as ``xOutlineModel`` and ``zOutlineModel`` but for the reference - panel - - xOutlineDiff, zOutlineDiff : numpy.ndarray, optional - Same as ``xOutlineModel`` and ``zOutlineModel`` but for the difference - panel - colorMapSectionName : str section name in ``config`` where color map info can be found. @@ -410,9 +379,6 @@ def plot_vertical_section_comparison( colorMapSectionName, xCoords=xCoords, zCoord=zCoord, - triangulation_args=triangulation_args, - xOutline=xOutlineModel, - zOutline=zOutlineModel, suffix=resultSuffix, colorbarLabel=colorbarLabel, title=title, @@ -461,9 +427,6 @@ def plot_vertical_section_comparison( colorMapSectionName, xCoords=xCoords, zCoord=zCoord, - triangulation_args=triangulation_args, - xOutline=xOutlineRef, - zOutline=zOutlineRef, suffix=resultSuffix, colorbarLabel=colorbarLabel, title=refTitle, @@ -504,9 +467,6 @@ def plot_vertical_section_comparison( colorMapSectionName, xCoords=xCoords, zCoord=zCoord, - triangulation_args=triangulation_args, - xOutline=xOutlineDiff, - zOutline=zOutlineDiff, suffix=diffSuffix, colorbarLabel=colorbarLabel, title=diffTitle, @@ -557,9 +517,6 @@ def plot_vertical_section( colorMapSectionName, xCoords=None, zCoord=None, - triangulation_args=None, - xOutline=None, - zOutline=None, suffix='', colorbarLabel=None, title=None, @@ -639,22 +596,6 @@ def plot_vertical_section( zCoord : xarray.DataArray, optional The z coordinates for the ``field`` - triangulation_args : dict, optional - A dict of arguments to create a matplotlib.tri.Triangulation of the - transect that does not rely on it being on a logically rectangular grid. - The arguments rather than the triangulation itself are passed because - multiple triangulations with different masks are needed internally and - there is not an obvious mechanism for copying an existing triangulation. - If this option is provided, ``xCoords`` is only used for tick marks if - more than one x axis is requested, and ``zCoord`` will be ignored. - - xOutline, zOutline : numpy.ndarray, optional - pairs of points defining line segments that are used to outline the - valid region of the mesh if ``outlineValid = True`` and - ``triangulation_args`` is not ``None`` - - - suffix : str, optional the suffix used for colorbar config options @@ -849,68 +790,38 @@ def plot_vertical_section( if len(xCoords) != len(xlabels): raise ValueError('Expected the same number of xCoords and xlabels') - if triangulation_args is None: - - x, y = xr.broadcast(xCoords[0], zCoord) - dims_in_field = all([dim in field.dims for dim in x.dims]) + x, y = xr.broadcast(xCoords[0], zCoord) + dims_in_field = all([dim in field.dims for dim in x.dims]) - if dims_in_field: - x = x.transpose(*field.dims) - y = y.transpose(*field.dims) - else: - xsize = list(x.sizes.values()) - fieldsize = list(field.sizes.values()) - if xsize[0] == fieldsize[0] + 1 and xsize[1] == fieldsize[1] + 1: - pass - elif xsize[0] == fieldsize[1] + 1 and xsize[1] == fieldsize[0] + 1: - x = x.transpose(x.dims[1], x.dims[0]) - y = y.transpose(y.dims[1], y.dims[0]) - else: - raise ValueError('Sizes of coords {}x{} and field {}x{} not ' - 'compatible.'.format(xsize[0], xsize[1], - fieldsize[0], - fieldsize[1])) - - # compute moving averages with respect to the x dimension - if movingAveragePoints is not None and movingAveragePoints != 1: - dim = field.dims[0] - field = field.rolling(dim={dim: movingAveragePoints}, - center=True).mean().dropna(dim, how='all') - x = x.rolling(dim={dim: movingAveragePoints}, - center=True).mean().dropna(dim, how='all') - y = y.rolling(dim={dim: movingAveragePoints}, - center=True).mean().dropna(dim, how='all') - - mask = field.notnull() - maskedTriangulation, unmaskedTriangulation = _get_triangulation( - x, y, mask) - if contourComparisonField is not None: - mask = field.notnull() - maskedComparisonTriangulation, _ = _get_triangulation(x, y, mask) - else: - maskedComparisonTriangulation = None + if dims_in_field: + x = x.transpose(*field.dims) + y = y.transpose(*field.dims) else: - mask = field.notnull() - triMask = np.logical_not(mask.values) - # if any node of a triangle is masked, the triangle is masked - triMask = np.amax(triMask, axis=1) - unmaskedTriangulation = Triangulation(**triangulation_args) - anythingToPlot = not np.all(triMask) - if anythingToPlot: - mask_args = dict(triangulation_args) - mask_args['mask'] = triMask - maskedTriangulation = Triangulation(**mask_args) - else: - maskedTriangulation = None - if contourComparisonField is not None: - mask = contourComparisonField.notnull() - triMask = np.logical_not(mask.values) - triMask = np.amax(triMask, axis=1) - mask_args = dict(triangulation_args) - mask_args['mask'] = triMask - maskedComparisonTriangulation = Triangulation(**mask_args) + xsize = list(x.sizes.values()) + fieldsize = list(field.sizes.values()) + if xsize[0] == fieldsize[0] + 1 and xsize[1] == fieldsize[1] + 1: + pass + elif xsize[0] == fieldsize[1] + 1 and xsize[1] == fieldsize[0] + 1: + x = x.transpose(x.dims[1], x.dims[0]) + y = y.transpose(y.dims[1], y.dims[0]) else: - maskedComparisonTriangulation = None + raise ValueError(f'Sizes of coords {xsize[0]}x{xsize[1]} and ' + f'field {fieldsize[0]}x{fieldsize[1]} not ' + f'compatible.') + + # compute moving averages with respect to the x dimension + if movingAveragePoints is not None and movingAveragePoints != 1: + dim = field.dims[0] + field = field.rolling(dim={dim: movingAveragePoints}, + center=True).mean().dropna(dim, how='all') + x = x.rolling(dim={dim: movingAveragePoints}, + center=True).mean().dropna(dim, how='all') + y = y.rolling(dim={dim: movingAveragePoints}, + center=True).mean().dropna(dim, how='all') + + mask = field.notnull() + + anythingToPlot = np.any(mask) # set up figure if dpi is None: @@ -926,27 +837,25 @@ def plot_vertical_section( # fill the unmasked region with the invalid color so it will show through # any masked regions zeroArray = xr.zeros_like(field) - plt.tricontourf(unmaskedTriangulation, zeroArray.values.ravel(), - colors=invalidColor) + plt.contourf(x.values, y.values, zeroArray.values, + colors=invalidColor) - if maskedTriangulation is not None: + if anythingToPlot: # there's something to plot if not plotAsContours: # display a heatmap of fieldArray - fieldMasked = field.where(mask, 0.0).values.ravel() - if colormapDict['levels'] is None: - plotHandle = plt.tripcolor(maskedTriangulation, fieldMasked, - cmap=colormapDict['colormap'], - norm=colormapDict['norm'], - rasterized=True, shading='gouraud') + plotHandle = plt.pcolormesh(x.values, y.values, field.values, + cmap=colormapDict['colormap'], + norm=colormapDict['norm'], + rasterized=True, shading='gouraud') else: - plotHandle = plt.tricontourf(maskedTriangulation, fieldMasked, - cmap=colormapDict['colormap'], - norm=colormapDict['norm'], - levels=colormapDict['levels'], - extend='both') + plotHandle = plt.contourf(x.values, y.values, field.values, + cmap=colormapDict['colormap'], + norm=colormapDict['norm'], + levels=colormapDict['levels'], + extend='both') cbar = plt.colorbar(plotHandle, orientation='vertical', @@ -960,21 +869,16 @@ def plot_vertical_section( else: # display a white heatmap to get a white background for non-land zeroArray = xr.zeros_like(field) - plt.tricontourf(maskedTriangulation, zeroArray.values.ravel(), - colors='white') + plt.contourf(x.values, y.values, zeroArray.values, colors='white') ax = plt.gca() ax.set_facecolor(backgroundColor) if outlineValid: - if xOutline is not None and zOutline is not None: - # also outline the domain if provided - plt.plot(xOutline, zOutline, color='black', linewidth=1) - else: - # do a contour to outline the boundary between valid and invalid - # values - landMask = np.isnan(field.values).ravel() - plt.tricontour(unmaskedTriangulation, landMask, levels=[0.0001], - colors='black', linewidths=1) + # do a contour to outline the boundary between valid and invalid + # values + landMask = np.isnan(field.values) + plt.contour(x.values, y.values, landMask, levels=[0.0001], + colors='black', linewidths=1) # plot contours, if they were requested contourLevels = colormapDict['contours'] @@ -983,19 +887,19 @@ def plot_vertical_section( cs2 = None plotLegend = False - if contourLevels is not None and maskedTriangulation is not None: + if contourLevels is not None and anythingToPlot: if len(contourLevels) == 0: # automatic calculation of contour levels contourLevels = None - mask = field.notnull() - fieldMasked = field.where(mask, 0.0).values.ravel() - - cs1 = plt.tricontour(maskedTriangulation, fieldMasked, - levels=contourLevels, - colors=lineColor, - linestyles=lineStyle, - linewidths=lineWidth, - cmap=contourColormap) + + cs1 = plt.contour(x.values, + y.values, + field.values, + levels=contourLevels, + colors=lineColor, + linestyles=lineStyle, + linewidths=lineWidth, + cmap=contourColormap) if labelContours: fmt_string = "%%1.%df" % int(contourLabelPrecision) plt.clabel(cs1, fmt=fmt_string) @@ -1004,23 +908,23 @@ def plot_vertical_section( if comparisonContourLineWidth is None: comparisonContourLineWidth = lineWidth mask = contourComparisonField.notnull() - fieldMasked = contourComparisonField.where(mask, 0.0).values.ravel() - cs2 = plt.tricontour(maskedComparisonTriangulation, - fieldMasked, - levels=contourLevels, - colors=comparisonContourLineColor, - linestyles=comparisonContourLineStyle, - linewidths=comparisonContourLineWidth, - cmap=contourColormap) + cs2 = plt.contour(x.values, + y.values, + contourComparisonField.values, + levels=contourLevels, + colors=comparisonContourLineColor, + linestyles=comparisonContourLineStyle, + linewidths=comparisonContourLineWidth, + cmap=contourColormap) if labelContours: plt.clabel(cs2, fmt=fmt_string) plotLegend = (((lineColor is not None and comparisonContourLineColor is not None) or - (lineWidth is not None and + (lineWidth is not None and comparisonContourLineWidth is not None)) and - (plotAsContours and contourComparisonField is not None)) + (plotAsContours and contourComparisonField is not None)) if plotLegend: h1, _ = cs1.legend_elements() @@ -1098,7 +1002,8 @@ def plot_vertical_section( xticks = None if numUpperTicks is not None: xticks = np.linspace(xlimits[0], xlimits[1], numUpperTicks) - tickValues = np.interp(xticks, xCoords[0].values, xCoords[1].values) + tickValues = np.interp(xticks, xCoords[0].values, + xCoords[1].values) ax2.set_xticks(xticks) formatString = "{{0:.{:d}f}}{}".format( upperXAxisTickLabelPrecision, r'$\degree$') @@ -1120,49 +1025,3 @@ def plot_vertical_section( ax3.spines['top'].set_position(('outward', 36)) return fig, ax - - -def _get_triangulation(x, y, mask): - """divide each quad in the x/y mesh into 2 triangles""" - - nx = x.sizes[x.dims[0]] - 1 - ny = x.sizes[x.dims[1]] - 1 - nTriangles = 2 * nx * ny - - mask = mask.values - mask = np.logical_and(np.logical_and(mask[0:-1, 0:-1], mask[1:, 0:-1]), - np.logical_and(mask[0:-1, 1:], mask[1:, 1:])) - triMask = np.zeros((nx, ny, 2), bool) - triMask[:, :, 0] = np.logical_not(mask) - triMask[:, :, 1] = triMask[:, :, 0] - - triMask = triMask.ravel() - - xIndices, yIndices = np.meshgrid(np.arange(nx), np.arange(ny), - indexing='ij') - - tris = np.zeros((nx, ny, 2, 3), int) - # upper triangles: - tris[:, :, 0, 0] = (ny + 1) * xIndices + yIndices - tris[:, :, 0, 1] = (ny + 1) * (xIndices + 1) + yIndices - tris[:, :, 0, 2] = (ny + 1) * xIndices + yIndices + 1 - # lower triangle - tris[:, :, 1, 0] = (ny + 1) * xIndices + yIndices + 1 - tris[:, :, 1, 1] = (ny + 1) * (xIndices + 1) + yIndices - tris[:, :, 1, 2] = (ny + 1) * (xIndices + 1) + yIndices + 1 - - tris = tris.reshape((nTriangles, 3)) - - x = x.values.ravel() - y = y.values.ravel() - - anythingToPlot = not np.all(triMask) - if anythingToPlot: - maskedTriangulation = Triangulation(x=x, y=y, triangles=tris, - mask=triMask) - else: - maskedTriangulation = None - - unmaskedTriangulation = Triangulation(x=x, y=y, triangles=tris) - - return maskedTriangulation, unmaskedTriangulation From ba0a3d9f6db155561b153a0a00c39eed3bc230a4 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sun, 6 Apr 2025 20:32:44 -0500 Subject: [PATCH 015/116] Update mpas_analysis for pyremap 2.0.0 --- .../ocean/climatology_map_antarctic_melt.py | 4 +- mpas_analysis/ocean/climatology_map_argo.py | 4 +- mpas_analysis/ocean/climatology_map_bgc.py | 4 +- mpas_analysis/ocean/climatology_map_eke.py | 6 +- mpas_analysis/ocean/climatology_map_mld.py | 4 +- .../ocean/climatology_map_schmidtko.py | 2 +- mpas_analysis/ocean/climatology_map_ssh.py | 6 +- mpas_analysis/ocean/climatology_map_sss.py | 6 +- mpas_analysis/ocean/climatology_map_sst.py | 6 +- mpas_analysis/ocean/climatology_map_waves.py | 8 +-- mpas_analysis/ocean/climatology_map_woa.py | 4 +- .../ocean/compute_transects_subtask.py | 4 +- mpas_analysis/ocean/remap_sose_climatology.py | 2 +- .../sea_ice/climatology_map_berg_conc.py | 6 +- .../sea_ice/climatology_map_melting.py | 6 +- .../sea_ice/climatology_map_production.py | 6 +- .../sea_ice/climatology_map_sea_ice_conc.py | 6 +- .../sea_ice/climatology_map_sea_ice_thick.py | 6 +- .../shared/climatology/climatology.py | 62 +++++++++++-------- .../climatology/mpas_climatology_task.py | 2 +- .../remap_mpas_climatology_subtask.py | 29 ++++----- .../remap_observed_climatology_subtask.py | 6 +- .../plot/plot_climatology_map_subtask.py | 4 +- mpas_analysis/test/test_climatology.py | 20 +++--- .../test/test_remap_obs_clim_subtask.py | 6 +- .../remap_mld_obs.py | 24 ++++--- 26 files changed, 131 insertions(+), 112 deletions(-) diff --git a/mpas_analysis/ocean/climatology_map_antarctic_melt.py b/mpas_analysis/ocean/climatology_map_antarctic_melt.py index 34e55bb3f..f3956cd9c 100644 --- a/mpas_analysis/ocean/climatology_map_antarctic_melt.py +++ b/mpas_analysis/ocean/climatology_map_antarctic_melt.py @@ -407,7 +407,7 @@ def get_observation_descriptor(self, fileName): # stereographic coordinates projection = get_pyproj_projection(comparison_grid_name='antarctic') obsDescriptor = ProjectionGridDescriptor.read( - projection, fileName=fileName, xVarName='x', yVarName='y') + projection, filename=fileName, x_var_name='x', y_var_name='y') # update the mesh name to match the format used elsewhere in # MPAS-Analysis @@ -416,7 +416,7 @@ def get_observation_descriptor(self, fileName): width = 1e-3 * (x[-1] - x[0]) height = 1e-3 * (y[-1] - y[0]) res = 1e-3 * (x[1] - x[0]) - obsDescriptor.meshName = f'{width}x{height}km_{res}km_Antarctic_stereo' + obsDescriptor.mesh_name = f'{width}x{height}km_{res}km_Antarctic_stereo' return obsDescriptor diff --git a/mpas_analysis/ocean/climatology_map_argo.py b/mpas_analysis/ocean/climatology_map_argo.py index 5349b5262..03dc857e4 100644 --- a/mpas_analysis/ocean/climatology_map_argo.py +++ b/mpas_analysis/ocean/climatology_map_argo.py @@ -394,8 +394,8 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using Lat/Lon # coordinates obsDescriptor = LatLonGridDescriptor.read(ds=dsObs, - latVarName='latCoord', - lonVarName='lonCoord') + lat_var_name='latCoord', + lon_var_name='lonCoord') dsObs.close() return obsDescriptor diff --git a/mpas_analysis/ocean/climatology_map_bgc.py b/mpas_analysis/ocean/climatology_map_bgc.py index 2d2c81023..0d7555a88 100644 --- a/mpas_analysis/ocean/climatology_map_bgc.py +++ b/mpas_analysis/ocean/climatology_map_bgc.py @@ -346,8 +346,8 @@ def get_observation_descriptor(self, fileName): # coordinates dsObs = self.build_observational_dataset(fileName) obsDescriptor = LatLonGridDescriptor.read(ds=dsObs, - latVarName='lat', - lonVarName='lon') + lat_var_name='lat', + lon_var_name='lon') return obsDescriptor def build_observational_dataset(self, fileName): diff --git a/mpas_analysis/ocean/climatology_map_eke.py b/mpas_analysis/ocean/climatology_map_eke.py index cea9c38e0..28a75cc43 100644 --- a/mpas_analysis/ocean/climatology_map_eke.py +++ b/mpas_analysis/ocean/climatology_map_eke.py @@ -239,9 +239,9 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using the lat/lon # coordinates - obsDescriptor = LatLonGridDescriptor.read(fileName=fileName, - latVarName='Lat', - lonVarName='Lon') + obsDescriptor = LatLonGridDescriptor.read(filename=fileName, + lat_var_name='Lat', + lon_var_name='Lon') return obsDescriptor diff --git a/mpas_analysis/ocean/climatology_map_mld.py b/mpas_analysis/ocean/climatology_map_mld.py index 0d4fbba67..4b1b4de3d 100644 --- a/mpas_analysis/ocean/climatology_map_mld.py +++ b/mpas_analysis/ocean/climatology_map_mld.py @@ -194,8 +194,8 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using the lat/lon # coordinates obsDescriptor = LatLonGridDescriptor.read(ds=dsObs, - latVarName='lat', - lonVarName='lon') + lat_var_name='lat', + lon_var_name='lon') dsObs.close() return obsDescriptor diff --git a/mpas_analysis/ocean/climatology_map_schmidtko.py b/mpas_analysis/ocean/climatology_map_schmidtko.py index 903e11002..3320c94bc 100644 --- a/mpas_analysis/ocean/climatology_map_schmidtko.py +++ b/mpas_analysis/ocean/climatology_map_schmidtko.py @@ -270,7 +270,7 @@ def get_observation_descriptor(self, fileName): mesh_name = ds_obs.attrs['meshName'] obs_descriptor = ProjectionGridDescriptor.create( - projection, x=x, y=y, meshName=mesh_name) + projection, x=x, y=y, mesh_name=mesh_name) return obs_descriptor def build_observational_dataset(self, fileName): diff --git a/mpas_analysis/ocean/climatology_map_ssh.py b/mpas_analysis/ocean/climatology_map_ssh.py index 93462208e..f52f39794 100644 --- a/mpas_analysis/ocean/climatology_map_ssh.py +++ b/mpas_analysis/ocean/climatology_map_ssh.py @@ -213,9 +213,9 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using the lat/lon # coordinates - obsDescriptor = LatLonGridDescriptor.read(fileName=fileName, - latVarName='lat', - lonVarName='lon') + obsDescriptor = LatLonGridDescriptor.read(filename=fileName, + lat_var_name='lat', + lon_var_name='lon') return obsDescriptor def build_observational_dataset(self, fileName): diff --git a/mpas_analysis/ocean/climatology_map_sss.py b/mpas_analysis/ocean/climatology_map_sss.py index f737d3bc0..2c074b4c2 100644 --- a/mpas_analysis/ocean/climatology_map_sss.py +++ b/mpas_analysis/ocean/climatology_map_sss.py @@ -172,9 +172,9 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using the lat/lon # coordinates - obsDescriptor = LatLonGridDescriptor.read(fileName=fileName, - latVarName='lat', - lonVarName='lon') + obsDescriptor = LatLonGridDescriptor.read(filename=fileName, + lat_var_name='lat', + lon_var_name='lon') return obsDescriptor def build_observational_dataset(self, fileName): diff --git a/mpas_analysis/ocean/climatology_map_sst.py b/mpas_analysis/ocean/climatology_map_sst.py index f7c901a5c..d05dfeebe 100644 --- a/mpas_analysis/ocean/climatology_map_sst.py +++ b/mpas_analysis/ocean/climatology_map_sst.py @@ -181,9 +181,9 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using the lat/lon # coordinates - obsDescriptor = LatLonGridDescriptor.read(fileName=fileName, - latVarName='lat', - lonVarName='lon') + obsDescriptor = LatLonGridDescriptor.read(filename=fileName, + lat_var_name='lat', + lon_var_name='lon') return obsDescriptor def build_observational_dataset(self, fileName): diff --git a/mpas_analysis/ocean/climatology_map_waves.py b/mpas_analysis/ocean/climatology_map_waves.py index da139981a..0f31c6fb2 100644 --- a/mpas_analysis/ocean/climatology_map_waves.py +++ b/mpas_analysis/ocean/climatology_map_waves.py @@ -374,8 +374,8 @@ def get_observation_descriptor(self, fileName): # {{{ # coordinates dsObs = self.build_observational_dataset(fileName) obsDescriptor = LatLonGridDescriptor.read(ds=dsObs, - latVarName='latitude', - lonVarName='longitude') + lat_var_name='latitude', + lon_var_name='longitude') return obsDescriptor # }}} def build_observational_dataset(self, fileName): # {{{ @@ -495,8 +495,8 @@ def get_observation_descriptor(self, fileName): # {{{ # coordinates dsObs = self.build_observational_dataset(fileName) obsDescriptor = LatLonGridDescriptor.read(ds=dsObs, - latVarName='lat', - lonVarName='lon') + lat_var_name='lat', + lon_var_name='lon') return obsDescriptor # }}} def build_observational_dataset(self, fileName): # {{{ diff --git a/mpas_analysis/ocean/climatology_map_woa.py b/mpas_analysis/ocean/climatology_map_woa.py index e7e34c1d1..35eb03193 100644 --- a/mpas_analysis/ocean/climatology_map_woa.py +++ b/mpas_analysis/ocean/climatology_map_woa.py @@ -310,8 +310,8 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using Lat/Lon # coordinates obsDescriptor = LatLonGridDescriptor.read(ds=dsObs, - latVarName='lat', - lonVarName='lon') + lat_var_name='lat', + lon_var_name='lon') dsObs.close() return obsDescriptor diff --git a/mpas_analysis/ocean/compute_transects_subtask.py b/mpas_analysis/ocean/compute_transects_subtask.py index 14cd4ff7b..b12ca7c9b 100644 --- a/mpas_analysis/ocean/compute_transects_subtask.py +++ b/mpas_analysis/ocean/compute_transects_subtask.py @@ -211,8 +211,8 @@ def setup_and_check(self): 'data': x}) self.collectionDescriptor = PointCollectionDescriptor( - lats, lons, collectionName=self.transectCollectionName, - units='degrees', outDimension='nPoints') + lats, lons, collection_name=self.transectCollectionName, + units='degrees', out_dimension='nPoints') self.add_comparison_grid_descriptor(self.transectCollectionName, self.collectionDescriptor) diff --git a/mpas_analysis/ocean/remap_sose_climatology.py b/mpas_analysis/ocean/remap_sose_climatology.py index fd188c85f..ad09fa7d4 100644 --- a/mpas_analysis/ocean/remap_sose_climatology.py +++ b/mpas_analysis/ocean/remap_sose_climatology.py @@ -114,7 +114,7 @@ def get_observation_descriptor(self, fileName): # stereographic coordinates projection = get_pyproj_projection(comparison_grid_name='antarctic') obsDescriptor = ProjectionGridDescriptor.read( - projection, fileName=fileName, xVarName='x', yVarName='y') + projection, filename=fileName, x_var_name='x', y_var_name='y') return obsDescriptor def build_observational_dataset(self, fileName): diff --git a/mpas_analysis/sea_ice/climatology_map_berg_conc.py b/mpas_analysis/sea_ice/climatology_map_berg_conc.py index 68bca886b..942c5d339 100644 --- a/mpas_analysis/sea_ice/climatology_map_berg_conc.py +++ b/mpas_analysis/sea_ice/climatology_map_berg_conc.py @@ -193,9 +193,9 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using the lat/lon # coordinates - obsDescriptor = LatLonGridDescriptor.read(fileName=fileName, - latVarName='latitude', - lonVarName='longitude') + obsDescriptor = LatLonGridDescriptor.read(filename=fileName, + lat_var_name='latitude', + lon_var_name='longitude') return obsDescriptor def build_observational_dataset(self, fileName): diff --git a/mpas_analysis/sea_ice/climatology_map_melting.py b/mpas_analysis/sea_ice/climatology_map_melting.py index 4eeec5b24..0bd1efde7 100755 --- a/mpas_analysis/sea_ice/climatology_map_melting.py +++ b/mpas_analysis/sea_ice/climatology_map_melting.py @@ -295,9 +295,9 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using the lat/lon # coordinates - obsDescriptor = LatLon2DGridDescriptor.read(fileName=fileName, - latVarName='lat', - lonVarName='lon') + obsDescriptor = LatLon2DGridDescriptor.read(filename=fileName, + lat_var_name='lat', + lon_var_name='lon') return obsDescriptor def build_observational_dataset(self, fileName): diff --git a/mpas_analysis/sea_ice/climatology_map_production.py b/mpas_analysis/sea_ice/climatology_map_production.py index d151bded3..4f69e49e4 100755 --- a/mpas_analysis/sea_ice/climatology_map_production.py +++ b/mpas_analysis/sea_ice/climatology_map_production.py @@ -295,9 +295,9 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using the lat/lon # coordinates - obsDescriptor = LatLon2DGridDescriptor.read(fileName=fileName, - latVarName='lat', - lonVarName='lon') + obsDescriptor = LatLon2DGridDescriptor.read(filename=fileName, + lat_var_name='lat', + lon_var_name='lon') return obsDescriptor def build_observational_dataset(self, fileName): diff --git a/mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py b/mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py index 94911b02e..f74ad713f 100644 --- a/mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py +++ b/mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py @@ -265,9 +265,9 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using the lat/lon # coordinates - obsDescriptor = LatLonGridDescriptor.read(fileName=fileName, - latVarName='t_lat', - lonVarName='t_lon') + obsDescriptor = LatLonGridDescriptor.read(filename=fileName, + lat_var_name='t_lat', + lon_var_name='t_lon') return obsDescriptor def build_observational_dataset(self, fileName): diff --git a/mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py b/mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py index 851ee4a7b..f165d3f15 100644 --- a/mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py +++ b/mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py @@ -200,9 +200,9 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using the lat/lon # coordinates - obsDescriptor = LatLonGridDescriptor.read(fileName=fileName, - latVarName='t_lat', - lonVarName='t_lon') + obsDescriptor = LatLonGridDescriptor.read(filename=fileName, + lat_var_name='t_lat', + lon_var_name='t_lon') return obsDescriptor def build_observational_dataset(self, fileName): diff --git a/mpas_analysis/shared/climatology/climatology.py b/mpas_analysis/shared/climatology/climatology.py index b1ff93871..82305e11c 100644 --- a/mpas_analysis/shared/climatology/climatology.py +++ b/mpas_analysis/shared/climatology/climatology.py @@ -84,10 +84,10 @@ def get_remapper(config, sourceDescriptor, comparisonDescriptor, # we need to remap because the grids don't match if vertices: - srcMeshName = f'{sourceDescriptor.meshName}_vertices' + srcMeshName = f'{sourceDescriptor.mesh_name}_vertices' else: - srcMeshName = sourceDescriptor.meshName - destMeshName = comparisonDescriptor.meshName + srcMeshName = sourceDescriptor.mesh_name + destMeshName = comparisonDescriptor.mesh_name mappingBaseName = \ f'{mappingFilePrefix}_{srcMeshName}_to_{destMeshName}_{method}.nc' @@ -119,22 +119,33 @@ def get_remapper(config, sourceDescriptor, comparisonDescriptor, make_directories(mappingSubdirectory) mappingFileName = f'{mappingSubdirectory}/{mappingBaseName}' - remapper = Remapper(sourceDescriptor, comparisonDescriptor, - mappingFileName) - mpiTasks = config.getint('execute', 'mapMpiTasks') esmf_parallel_exec = config.get('execute', 'mapParallelExec') if esmf_parallel_exec == 'None': esmf_parallel_exec = None - mappingSubdirectory = \ - build_config_full_path(config, 'output', - 'mappingSubdirectory') - make_directories(mappingSubdirectory) - with TemporaryDirectory(dir=mappingSubdirectory) as tempdir: - remapper.build_mapping_file(method=method, logger=logger, - mpiTasks=mpiTasks, tempdir=tempdir, - esmf_parallel_exec=esmf_parallel_exec) + remapper = Remapper( + ntasks=mpiTasks, + map_filename=mappingFileName, + method=method, + parallel_exec=esmf_parallel_exec, + src_descriptor=sourceDescriptor, + dst_descriptor=comparisonDescriptor, + ) + + if mappingFileName is not None and not os.path.exists(mappingFileName): + mappingSubdirectory = \ + build_config_full_path( + config, 'output', 'mappingSubdirectory') + make_directories(mappingSubdirectory) + with TemporaryDirectory(dir=mappingSubdirectory) as tempdir: + remapper.src_scrip_filename = os.path.join( + tempdir, remapper.src_scrip_filename) + remapper.dst_scrip_filename = os.path.join( + tempdir, remapper.dst_scrip_filename) + + # TEMP: logger not supported in this RC + remapper.build_map(logger=logger) return remapper @@ -358,7 +369,7 @@ def remap_and_write_climatology(config, climatologyDataSet, useNcremap = config.getboolean('climatology', 'useNcremap') - if remapper.mappingFileName is None: + if remapper.map_filename is None: # no remapping is needed remappedClimatology = climatologyDataSet else: @@ -372,17 +383,18 @@ def remap_and_write_climatology(config, climatologyDataSet, if useNcremap: if not os.path.exists(climatologyFileName): write_netcdf(climatologyDataSet, climatologyFileName) - remapper.remap_file(inFileName=climatologyFileName, - outFileName=remappedFileName, - overwrite=True, - renormalize=renormalizationThreshold, - logger=logger, - parallel_exec=parallel_exec) + remapper.ncremap( + in_filename=climatologyFileName, + out_filename=remappedFileName, + overwrite=True, + renormalize=renormalizationThreshold, + logger=logger, + parallel_exec=parallel_exec) remappedClimatology = xr.open_dataset(remappedFileName) else: - remappedClimatology = remapper.remap(climatologyDataSet, - renormalizationThreshold) + remappedClimatology = remapper.remap_numpy( + climatologyDataSet, renormalizationThreshold) write_netcdf_with_fill(remappedClimatology, remappedFileName) return remappedClimatology @@ -584,7 +596,7 @@ def get_remapped_mpas_climatology_file_name(config, season, componentName, if comparisonGridName in known_comparison_grids: comparisonDescriptor = get_comparison_descriptor(config, comparisonGridName) - comparisonFullMeshName = comparisonDescriptor.meshName + comparisonFullMeshName = comparisonDescriptor.mesh_name else: comparisonFullMeshName = comparisonGridName.replace(' ', '_') @@ -677,7 +689,7 @@ def _matches_comparison(obsDescriptor, comparisonDescriptor): isinstance(comparisonDescriptor, ProjectionGridDescriptor): # pretty hard to determine if projections are the same, so we'll rely # on the grid names - match = obsDescriptor.meshName == comparisonDescriptor.meshName and \ + match = obsDescriptor.mesh_name == comparisonDescriptor.mesh_name and \ len(obsDescriptor.x) == len(comparisonDescriptor.x) and \ len(obsDescriptor.y) == len(comparisonDescriptor.y) and \ numpy.all(numpy.isclose(obsDescriptor.x, diff --git a/mpas_analysis/shared/climatology/mpas_climatology_task.py b/mpas_analysis/shared/climatology/mpas_climatology_task.py index 2ac27c0ed..921f0da56 100644 --- a/mpas_analysis/shared/climatology/mpas_climatology_task.py +++ b/mpas_analysis/shared/climatology/mpas_climatology_task.py @@ -506,7 +506,7 @@ def _compute_climatologies_with_ncclimo(self, inDirectory, outDirectory, '-o', outDirectory] + inFiles if remapper is not None: - args.extend(['-r', remapper.mappingFileName]) + args.extend(['-r', remapper.map_filename]) if remappedDirectory is not None: args.extend(['-O', remappedDirectory]) diff --git a/mpas_analysis/shared/climatology/remap_mpas_climatology_subtask.py b/mpas_analysis/shared/climatology/remap_mpas_climatology_subtask.py index 00c286b8e..58fd2e394 100644 --- a/mpas_analysis/shared/climatology/remap_mpas_climatology_subtask.py +++ b/mpas_analysis/shared/climatology/remap_mpas_climatology_subtask.py @@ -415,15 +415,15 @@ def _setup_remappers(self): for comparisonGridName in self.comparisonDescriptors: comparisonDescriptor = \ self.comparisonDescriptors[comparisonGridName] - self.comparisonGridName = comparisonDescriptor.meshName + self.comparisonGridName = comparisonDescriptor.mesh_name meshName = config.get('input', 'mpasMeshName') if self.vertices: mpasDescriptor = MpasVertexMeshDescriptor( - self.restartFileName, meshName=meshName) + self.restartFileName, mesh_name=meshName) else: mpasDescriptor = MpasCellMeshDescriptor( - self.restartFileName, meshName=meshName) - self.mpasMeshName = mpasDescriptor.meshName + self.restartFileName, mesh_name=meshName) + self.mpasMeshName = mpasDescriptor.mesh_name self.remappers[comparisonGridName] = get_remapper( config=config, sourceDescriptor=mpasDescriptor, @@ -451,7 +451,7 @@ def _setup_file_names(self): comparisonDescriptor = \ self.comparisonDescriptors[comparisonGridName] comparisonFullMeshNames[comparisonGridName] = \ - comparisonDescriptor.meshName + comparisonDescriptor.mesh_name keys = [] for season in self.seasons: @@ -588,7 +588,7 @@ def _remap(self, inFileName, outFileName, remapper, comparisonGridName, # ------- # Xylar Asay-Davis - if remapper.mappingFileName is None: + if remapper.map_filename is None: # no remapping is needed return @@ -603,20 +603,21 @@ def _remap(self, inFileName, outFileName, remapper, comparisonGridName, if self.useNcremap: basename, ext = os.path.splitext(outFileName) ncremapFilename = f'{basename}_ncremap{ext}' - remapper.remap_file(inFileName=inFileName, - outFileName=ncremapFilename, - overwrite=True, - renormalize=renormalizationThreshold, - logger=self.logger, - parallel_exec=parallel_exec) + remapper.ncremap( + in_filename=inFileName, + out_filename=ncremapFilename, + overwrite=True, + renormalize=renormalizationThreshold, + logger=self.logger, + parallel_exec=parallel_exec) remappedClimatology = xr.open_dataset(ncremapFilename) else: climatologyDataSet = xr.open_dataset(inFileName) - remappedClimatology = remapper.remap(climatologyDataSet, - renormalizationThreshold) + remappedClimatology = remapper.remap_numpy( + climatologyDataSet, renormalizationThreshold) # customize (if this function has been overridden) remappedClimatology = self.customize_remapped_climatology( diff --git a/mpas_analysis/shared/climatology/remap_observed_climatology_subtask.py b/mpas_analysis/shared/climatology/remap_observed_climatology_subtask.py index 3deebaab1..e97bad18d 100644 --- a/mpas_analysis/shared/climatology/remap_observed_climatology_subtask.py +++ b/mpas_analysis/shared/climatology/remap_observed_climatology_subtask.py @@ -175,7 +175,7 @@ def run_task(self): remapper = self.remappers[comparisonGridName] - if remapper.mappingFileName is None: + if remapper.map_filename is None: # no need to remap because the observations are on the # comparison grid already os.symlink(climatologyFileName, remappedFileName) @@ -266,7 +266,7 @@ def get_file_name(self, stage, season=None, comparisonGridName=None): else: remapper = self.remappers[comparisonGridName] - obsGridName = remapper.sourceDescriptor.meshName + obsGridName = remapper.src_descriptor.mesh_name outFilePrefix = self.outFilePrefix @@ -293,7 +293,7 @@ def get_file_name(self, stage, season=None, comparisonGridName=None): make_directories(remappedDirectory) - comparisonGridName = remapper.destinationDescriptor.meshName + comparisonGridName = remapper.dst_descriptor.mesh_name fileName = '{}/{}_{}_to_{}_{}.nc'.format( remappedDirectory, outFilePrefix, obsGridName, comparisonGridName, season) diff --git a/mpas_analysis/shared/plot/plot_climatology_map_subtask.py b/mpas_analysis/shared/plot/plot_climatology_map_subtask.py index 7db492d19..59d1b341b 100644 --- a/mpas_analysis/shared/plot/plot_climatology_map_subtask.py +++ b/mpas_analysis/shared/plot/plot_climatology_map_subtask.py @@ -603,8 +603,8 @@ def _plot_projection(self, remappedModelClimatology, comparisonDescriptor = get_comparison_descriptor( config, comparisonGridName) - x = comparisonDescriptor.xCorner - y = comparisonDescriptor.yCorner + x = comparisonDescriptor.x_corner + y = comparisonDescriptor.y_corner aspectRatio = (x[-1] - x[0])/(y[-1] - y[0]) diff --git a/mpas_analysis/test/test_climatology.py b/mpas_analysis/test/test_climatology.py index 2b6e2ce11..0d9c79168 100644 --- a/mpas_analysis/test/test_climatology.py +++ b/mpas_analysis/test/test_climatology.py @@ -84,7 +84,7 @@ def setup_mpas_remapper(self, config): get_comparison_descriptor(config, comparison_grid_name='latlon') mpasDescriptor = MpasCellMeshDescriptor( - mpasMeshFileName, meshName=config.get('input', 'mpasMeshName')) + mpasMeshFileName, mesh_name=config.get('input', 'mpasMeshName')) remapper = get_remapper( config=config, sourceDescriptor=mpasDescriptor, @@ -100,9 +100,9 @@ def setup_obs_remapper(self, config, fieldName): comparisonDescriptor = \ get_comparison_descriptor(config, comparison_grid_name='latlon') - obsDescriptor = LatLonGridDescriptor.read(fileName=gridFileName, - latVarName='lat', - lonVarName='lon') + obsDescriptor = LatLonGridDescriptor.read(filename=gridFileName, + lat_var_name='lat', + lon_var_name='lon') remapper = \ get_remapper( @@ -151,12 +151,12 @@ def test_get_mpas_remapper(self): remapper = self.setup_mpas_remapper(config) assert (os.path.abspath(mappingFileName) == - os.path.abspath(remapper.mappingFileName)) + os.path.abspath(remapper.map_filename)) assert os.path.exists(mappingFileName) - assert isinstance(remapper.sourceDescriptor, + assert isinstance(remapper.src_descriptor, MpasCellMeshDescriptor) - assert isinstance(remapper.destinationDescriptor, + assert isinstance(remapper.dst_descriptor, LatLonGridDescriptor) if not setName: @@ -182,12 +182,12 @@ def test_get_observations_remapper(self): remapper = self.setup_obs_remapper(config, fieldName) assert (os.path.abspath(mappingFileName) == - os.path.abspath(remapper.mappingFileName)) + os.path.abspath(remapper.map_filename)) assert os.path.exists(mappingFileName) - assert isinstance(remapper.sourceDescriptor, + assert isinstance(remapper.src_descriptor, LatLonGridDescriptor) - assert isinstance(remapper.destinationDescriptor, + assert isinstance(remapper.dst_descriptor, LatLonGridDescriptor) if not setName: diff --git a/mpas_analysis/test/test_remap_obs_clim_subtask.py b/mpas_analysis/test/test_remap_obs_clim_subtask.py index 378356927..905b21ba8 100644 --- a/mpas_analysis/test/test_remap_obs_clim_subtask.py +++ b/mpas_analysis/test/test_remap_obs_clim_subtask.py @@ -59,9 +59,9 @@ def get_observation_descriptor(self, fileName): # create a descriptor of the observation grid using the lat/lon # coordinates - obsDescriptor = LatLonGridDescriptor.read(fileName=fileName, - latVarName='lat', - lonVarName='lon') + obsDescriptor = LatLonGridDescriptor.read(filename=fileName, + lat_var_name='lat', + lon_var_name='lon') return obsDescriptor def build_observational_dataset(self, fileName): diff --git a/mpas_analysis/test/test_remap_obs_clim_subtask/remap_mld_obs.py b/mpas_analysis/test/test_remap_obs_clim_subtask/remap_mld_obs.py index 0d6751de0..06624f2fc 100644 --- a/mpas_analysis/test/test_remap_obs_clim_subtask/remap_mld_obs.py +++ b/mpas_analysis/test/test_remap_obs_clim_subtask/remap_mld_obs.py @@ -16,9 +16,9 @@ inputFileName = '/media/xylar/extra_data/analysis/output/GMPAS-QU240/' \ 'remap_obs/clim/obs/mld_1.0x1.0degree.nc' -obsDescriptor = LatLonGridDescriptor.read(fileName=inputFileName, - latVarName='lat', - lonVarName='lon') +obsDescriptor = LatLonGridDescriptor.read(filename=inputFileName, + lat_var_name='lat', + lon_var_name='lon') comparisonLatRes = 4. comparisonLonRes = 4. @@ -30,11 +30,17 @@ comparisonDescriptor = LatLonGridDescriptor.create(lat, lon, units='degrees') -remapper = Remapper(obsDescriptor, comparisonDescriptor, - mappingFileName='map.nc') +remapper = Remapper( + map_filename='map.nc', + src_descriptor=obsDescriptor, + dst_descriptor=comparisonDescriptor, +) -remapper.build_mapping_file() +remapper.build_map() -remapper.remap_file(inputFileName, 'mld_4.0x4.0degree.nc', - ['mld', 'month', 'year'], - renormalize=0.05) +remapper.ncremap( + inputFileName, + 'mld_4.0x4.0degree.nc', + ['mld', 'month', 'year'], + renormalize=0.05 +) From 84c3d9934de8236708ea5b3c3f55158e084c5a57 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sun, 6 Apr 2025 20:33:04 -0500 Subject: [PATCH 016/116] Update preprocessing for pyremap 2.0.0 --- .../preprocess_SOSE_data.py | 28 ++++++++++--------- .../preprocess_Schmidtko_data.py | 18 ++++++------ .../preprocess_adusumilli_melt.py | 11 +++++--- .../preprocess_paolo_melt.py | 11 +++++--- preprocess_observations/remap_rignot.py | 18 ++++++------ 5 files changed, 47 insertions(+), 39 deletions(-) diff --git a/preprocess_observations/preprocess_SOSE_data.py b/preprocess_observations/preprocess_SOSE_data.py index 3f6c16ea8..99d845398 100755 --- a/preprocess_observations/preprocess_SOSE_data.py +++ b/preprocess_observations/preprocess_SOSE_data.py @@ -32,8 +32,7 @@ import gsw from mpas_analysis.shared.io.download import download_files -from mpas_analysis.shared.interpolation import Remapper -from mpas_analysis.shared.grid import LatLonGridDescriptor +from pyremap import LatLonGridDescriptor, Remapper from mpas_analysis.shared.climatology.comparison_descriptors \ import get_comparison_descriptor from mpas_analysis.configuration \ @@ -486,24 +485,27 @@ def remap(ds, outDescriptor, mappingFileName, inDir, outFileName): ds.close() inDescriptor = LatLonGridDescriptor.read(tempFileName1, - latVarName='lat', - lonVarName='lon') + lat_var_name='lat', + lon_var_name='lon') - remapper = Remapper(inDescriptor, outDescriptor, mappingFileName) + remapper = Remapper(map_filename=mappingFileName, method='bilinear') + remapper.src_descriptor = inDescriptor + remapper.dst_descriptor = outDescriptor + remapper.build_map() - remapper.build_mapping_file(method='bilinear') - - remapper.remap_file(inFileName=tempFileName1, - outFileName=tempFileName2, - overwrite=True, - renormalize=0.01) + remapper.ncremap( + in_filename=tempFileName1, + out_filename=tempFileName2, + overwrite=True, + renormalize=0.01 + ) ds = xarray.open_dataset(tempFileName2) if 'z' in ds: print(' transposing back...') ds = ds.chunk({'Time': 4}) ds = ds.transpose('Time', 'x', 'y', 'z', 'nvertices') - ds.attrs['meshName'] = outDescriptor.meshName + ds.attrs['meshName'] = outDescriptor.mesh_name for coord in ['x', 'y']: ds.coords[coord] = xarray.DataArray.from_dict( @@ -765,7 +767,7 @@ def main(): '{}'.format(antarcticStereoWidth)) outDescriptor = get_comparison_descriptor(config, 'antarctic') - outGridName = '{}_{}'.format(outDescriptor.meshName, date) + outGridName = '{}_{}'.format(outDescriptor.mesh_name, date) inPrefixes = [inTPrefix, inSPrefix, inMLDPrefix, inUPrefix, inVPrefix, inGammaNPrefix] diff --git a/preprocess_observations/preprocess_Schmidtko_data.py b/preprocess_observations/preprocess_Schmidtko_data.py index 2a1d70236..200496a83 100755 --- a/preprocess_observations/preprocess_Schmidtko_data.py +++ b/preprocess_observations/preprocess_Schmidtko_data.py @@ -31,8 +31,7 @@ from mpas_analysis.shared.io.download import download_files -from mpas_analysis.shared.interpolation import Remapper -from mpas_analysis.shared.grid import LatLonGridDescriptor +from pyremap import LatLonGridDescriptor, Remapper from mpas_analysis.shared.climatology.comparison_descriptors \ import get_comparison_descriptor from mpas_analysis.configuration \ @@ -150,11 +149,11 @@ def remap(inDir, outDir): inDescriptor = LatLonGridDescriptor() inDescriptor = LatLonGridDescriptor.read(inFileName, - latVarName='lat', - lonVarName='lon') + lat_var_name='lat', + lon_var_name='lon') outDescriptor = get_comparison_descriptor(config, 'antarctic') - outGridName = outDescriptor.meshName + outGridName = outDescriptor.mesh_name outFileName = '{}/Schmidtko_et_al_2014_bottom_PT_S_PD_{}.nc'.format( outDir, outGridName) @@ -162,14 +161,15 @@ def remap(inDir, outDir): mappingFileName = '{}/map_{}_to_{}.nc'.format(inDir, inGridName, outGridName) - remapper = Remapper(inDescriptor, outDescriptor, mappingFileName) - - remapper.build_mapping_file(method='bilinear') + remapper = Remapper(map_filename=mappingFileName, method='bilinear') + remapper.src_descriptor = inDescriptor + remapper.dst_descriptor = outDescriptor + remapper.build_map() if not os.path.exists(outFileName): print('Remapping...') with xarray.open_dataset(inFileName) as dsIn: - with remapper.remap(dsIn, renormalizationThreshold=0.01) \ + with remapper.remap_numpy(dsIn, renormalizationThreshold=0.01) \ as remappedMLD: print('Done.') remappedMLD.attrs['history'] = ' '.join(sys.argv) diff --git a/preprocess_observations/preprocess_adusumilli_melt.py b/preprocess_observations/preprocess_adusumilli_melt.py index a0bb7db05..bd63e9a69 100755 --- a/preprocess_observations/preprocess_adusumilli_melt.py +++ b/preprocess_observations/preprocess_adusumilli_melt.py @@ -163,13 +163,16 @@ def remap_adusumilli(in_filename, out_prefix, date, task_count=512): map_filename = f'map_{in_grid_name}_to_{out_grid_name}_{method}.nc' - remapper = Remapper(in_descriptor, out_descriptor, map_filename) + remapper = Remapper( + ntasks=task_count, map_filename=map_filename, method=method) + remapper.src_descriptor = in_descriptor + remapper.dst_descriptor = out_descriptor + remapper.parallel_exec = 'srun' if not os.path.exists(map_filename): - remapper.build_mapping_file(method=method, mpiTasks=task_count, - esmf_parallel_exec='srun') + remapper.build_map() - ds_out = remapper.remap(ds) + ds_out = remapper.remap_numpy(ds) mask = ds_out.meltMask > 0. ds_out['meltRate'] = ds_out.meltRate.where(mask) ds_out.meltRate.attrs = melt_attrs diff --git a/preprocess_observations/preprocess_paolo_melt.py b/preprocess_observations/preprocess_paolo_melt.py index 91bca7475..4842dea66 100755 --- a/preprocess_observations/preprocess_paolo_melt.py +++ b/preprocess_observations/preprocess_paolo_melt.py @@ -158,13 +158,16 @@ def remap_paolo(in_filename, out_prefix, date, task_count=128): map_filename = f'map_{in_grid_name}_to_{out_grid_name}_{method}.nc' - remapper = Remapper(in_descriptor, out_descriptor, map_filename) + remapper = Remapper( + ntasks=task_count, map_filename=map_filename, method=method) + remapper.src_descriptor = in_descriptor + remapper.dst_descriptor = out_descriptor + remapper.parallel_exec = 'srun' if not os.path.exists(map_filename): - remapper.build_mapping_file(method=method, mpiTasks=task_count, - esmf_parallel_exec='srun') + remapper.build_map() - ds_out = remapper.remap(ds) + ds_out = remapper.remap_numpy(ds) mask = ds_out.meltMask > 0. ds_out['meltRate'] = ds_out.meltRate.where(mask) ds_out.meltRate.attrs = melt_attrs diff --git a/preprocess_observations/remap_rignot.py b/preprocess_observations/remap_rignot.py index 952de79e0..e2a53269a 100644 --- a/preprocess_observations/remap_rignot.py +++ b/preprocess_observations/remap_rignot.py @@ -15,8 +15,7 @@ import pyproj import sys -from mpas_analysis.shared.interpolation import Remapper -from mpas_analysis.shared.grid import ProjectionGridDescriptor +from pyremap import ProjectionGridDescriptor, Remapper from mpas_analysis.shared.mpas_xarray.mpas_xarray import subset_variables from mpas_analysis.shared.climatology \ import get_Antarctic_stereographic_comparison_descriptor @@ -50,21 +49,22 @@ inDescriptor = ProjectionGridDescriptor(projection) -inDescriptor.read(inFileName, xVarName='xaxis', yVarName='yaxis', - meshName=inGridName) +inDescriptor.read(inFileName, x_var_name='xaxis', y_var_name='yaxis', + mesh_name=inGridName) outDescriptor = get_Antarctic_stereographic_comparison_descriptor(config) -outGridName = outDescriptor.meshName +outGridName = outDescriptor.mesh_name outFileName = 'Rignot_2013_melt_rates_{}.nc'.format(outGridName) mappingFileName = 'map_{}_to_{}.nc'.format(inGridName, outGridName) -remapper = Remapper(inDescriptor, outDescriptor, mappingFileName) +remapper = Remapper(map_filename=mappingFileName, method='bilinear') +remapper.src_descriptor = inDescriptor +remapper.dst_descriptor = outDescriptor +remapper.build_map() -remapper.build_mapping_file(method='bilinear') - -remappedDataset = remapper.remap(ds, renormalizationThreshold=0.01) +remappedDataset = remapper.remap_numpy(ds, renormalizationThreshold=0.01) remappedDataset.attrs['history'] = ' '.join(sys.argv) remappedDataset.to_netcdf(outFileName) From 209f50a86990d2860eed5f84b8a73aefa3a091d7 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 7 Apr 2025 09:21:40 -0500 Subject: [PATCH 017/116] Update docs for pyremap 2.0.0 --- docs/tutorials/dev_add_task.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/dev_add_task.rst b/docs/tutorials/dev_add_task.rst index 5b7bc5072..a4b7474a5 100644 --- a/docs/tutorials/dev_add_task.rst +++ b/docs/tutorials/dev_add_task.rst @@ -417,8 +417,8 @@ And here's the one for plotting it: matplotlib.rc('font', size=14) - x = descriptor.xCorner - y = descriptor.yCorner + x = descriptor.x_corner + y = descriptor.y_corner extent = [x[0], x[-1], y[0], y[-1]] From 663b70c600a54ab1e01e1202511f1a4c87e54d5c Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sun, 13 Apr 2025 11:47:13 -0500 Subject: [PATCH 018/116] Fix hangs in ocean conservation A recent change switched to using xarray to concatinate ocean conservation data together. However, recent testing showed hangs if the datasets are not manually loaded. This merge adds 2 places where such manual loads are included. --- mpas_analysis/ocean/conservation.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mpas_analysis/ocean/conservation.py b/mpas_analysis/ocean/conservation.py index ecd251edf..50c915a65 100644 --- a/mpas_analysis/ocean/conservation.py +++ b/mpas_analysis/ocean/conservation.py @@ -769,6 +769,9 @@ def _compute_time_series_with_xarray(self, variable_list): unique_indices = sorted(unique_indices) # Ensure ascending order ds = ds.isel(Time=unique_indices) + # seeing hanging during saving. Let's try loading + ds.load() + if append: # Load the existing dataset and combine it with the new dataset self.logger.info( @@ -785,6 +788,9 @@ def _compute_time_series_with_xarray(self, variable_list): self.logger.info('Sorting by xtime...') ds = ds.sortby('xtime') + # again, seeing hanging during saving. Let's try loading + ds.load() + # Save the resulting dataset to the output file self.logger.info( f'Saving concatenated dataset to {self.outputFile}...') From e9d0bc6e387c40d699cad145af083b1e4e31f830 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sun, 9 Mar 2025 06:56:08 -0500 Subject: [PATCH 019/116] Switch to using barotropic streamfunction from `mpas_tools` --- mpas_analysis/ocean/climatology_map_bsf.py | 267 +++------------------ 1 file changed, 36 insertions(+), 231 deletions(-) diff --git a/mpas_analysis/ocean/climatology_map_bsf.py b/mpas_analysis/ocean/climatology_map_bsf.py index ddae4ade9..5a55cf516 100644 --- a/mpas_analysis/ocean/climatology_map_bsf.py +++ b/mpas_analysis/ocean/climatology_map_bsf.py @@ -11,14 +11,15 @@ import os import xarray as xr -import numpy as np -import scipy.sparse -import scipy.sparse.linalg + +from mpas_tools.ocean.barotropic_streamfunction import ( + compute_barotropic_streamfunction, + shift_barotropic_streamfunction +) from mpas_analysis.shared import AnalysisTask from mpas_analysis.shared.climatology import RemapMpasClimatologySubtask from mpas_analysis.shared.plot import PlotClimatologyMapSubtask -from mpas_analysis.ocean.utility import compute_zmid from mpas_analysis.shared.projection import comparison_grid_option_suffixes @@ -315,8 +316,30 @@ def customize_masked_climatology(self, climatology, season): 'edgesOnVertex', 'dcEdge', 'dvEdge', 'bottomDepth', 'maxLevelCell', 'latVertex', 'areaTriangle',]] ds_mesh.load() - bsf_vertex = self._compute_barotropic_streamfunction_vertex( - ds_mesh, climatology) + + cells_on_vertex = ds_mesh.cellsOnVertex - 1 + lat_vertex = ds_mesh.latVertex + bsf_vertex = compute_barotropic_streamfunction( + ds_mesh=ds_mesh, + ds=climatology, + min_depth=self.min_depth, + max_depth=self.max_depth, + include_bolus=self.include_bolus, + include_submesoscale=self.include_submesoscale, + logger=logger, + ) + + lat_range = config.getexpression( + self.taskName, 'latitudeRangeForZeroBSF') + + bsf_vertex = shift_barotropic_streamfunction( + bsf_vertex=bsf_vertex, + lat_range=lat_range, + cells_on_vertex=cells_on_vertex, + lat_vertex=lat_vertex, + logger=logger, + ) + logger.info('bsf on vertices computed.') climatology['barotropicStreamfunction'] = bsf_vertex @@ -340,234 +363,16 @@ def customize_masked_climatology(self, climatology, season): lat_range = config.getexpression( config_section_name, 'latitudeRangeForZeroBSF') - climatology[mpas_field_name] = _shift_bsf( - bsf_vertex, lat_range, ds_mesh.cellsOnVertex - 1, - ds_mesh.latVertex) + climatology[mpas_field_name] = shift_barotropic_streamfunction( + bsf_vertex=bsf_vertex, + lat_range=lat_range, + cells_on_vertex=cells_on_vertex, + lat_vertex=lat_vertex, + logger=logger, + ) climatology[mpas_field_name].attrs['units'] = 'Sv' climatology[mpas_field_name].attrs['description'] = \ f'barotropic streamfunction at vertices, offset for ' \ f'{grid_suffix} plots' return climatology - - def _compute_vert_integ_velocity(self, ds_mesh, ds): - - cells_on_edge = ds_mesh.cellsOnEdge - 1 - inner_edges = np.logical_and(cells_on_edge.isel(TWO=0) >= 0, - cells_on_edge.isel(TWO=1) >= 0) - - # convert from boolean mask to indices - inner_edges = np.flatnonzero(inner_edges.values) - - cell0 = cells_on_edge.isel(nEdges=inner_edges, TWO=0) - cell1 = cells_on_edge.isel(nEdges=inner_edges, TWO=1) - n_vert_levels = ds.sizes['nVertLevels'] - - layer_thickness = ds.timeMonthly_avg_layerThickness - max_level_cell = ds_mesh.maxLevelCell - 1 - - vert_index = xr.DataArray.from_dict( - {'dims': ('nVertLevels',), 'data': np.arange(n_vert_levels)}) - z_mid = compute_zmid(ds_mesh.bottomDepth, max_level_cell, - layer_thickness) - z_mid_edge = 0.5*(z_mid.isel(nCells=cell0) + - z_mid.isel(nCells=cell1)) - - normal_velocity = ds.timeMonthly_avg_normalVelocity - if self.include_bolus: - normal_velocity += ds.timeMonthly_avg_normalGMBolusVelocity - if self.include_submesoscale: - normal_velocity += ds.timeMonthly_avg_normalMLEvelocity - normal_velocity = normal_velocity.isel(nEdges=inner_edges) - - layer_thickness_edge = 0.5*(layer_thickness.isel(nCells=cell0) + - layer_thickness.isel(nCells=cell1)) - mask_bottom = (vert_index <= max_level_cell).T - mask_bottom_edge = np.logical_and(mask_bottom.isel(nCells=cell0), - mask_bottom.isel(nCells=cell1)) - masks = [mask_bottom_edge, - z_mid_edge <= self.min_depth, - z_mid_edge >= self.max_depth] - for mask in masks: - normal_velocity = normal_velocity.where(mask) - layer_thickness_edge = layer_thickness_edge.where(mask) - - vert_integ_velocity = np.zeros(ds_mesh.dims['nEdges'], dtype=float) - inner_vert_integ_vel = ( - (layer_thickness_edge * normal_velocity).sum(dim='nVertLevels')) - vert_integ_velocity[inner_edges] = inner_vert_integ_vel.values - - vert_integ_velocity = xr.DataArray(vert_integ_velocity, - dims=('nEdges',)) - - return vert_integ_velocity - - def _compute_edge_sign_on_vertex(self, ds_mesh): - edges_on_vertex = ds_mesh.edgesOnVertex - 1 - vertices_on_edge = ds_mesh.verticesOnEdge - 1 - - nvertices = ds_mesh.sizes['nVertices'] - vertex_degree = ds_mesh.sizes['vertexDegree'] - - edge_sign_on_vertex = np.zeros((nvertices, vertex_degree), dtype=int) - vertices = np.arange(nvertices) - for iedge in range(vertex_degree): - eov = edges_on_vertex.isel(vertexDegree=iedge) - valid_edge = eov >= 0 - - v0_on_edge = vertices_on_edge.isel(nEdges=eov, TWO=0) - v1_on_edge = vertices_on_edge.isel(nEdges=eov, TWO=1) - valid_edge = np.logical_and(valid_edge, v0_on_edge >= 0) - valid_edge = np.logical_and(valid_edge, v1_on_edge >= 0) - - mask = np.logical_and(valid_edge, v0_on_edge == vertices) - edge_sign_on_vertex[mask, iedge] = -1 - - mask = np.logical_and(valid_edge, v1_on_edge == vertices) - edge_sign_on_vertex[mask, iedge] = 1 - - return edge_sign_on_vertex - - def _compute_vert_integ_vorticity(self, ds_mesh, vert_integ_velocity, - edge_sign_on_vertex): - - area_vertex = ds_mesh.areaTriangle - dc_edge = ds_mesh.dcEdge - edges_on_vertex = ds_mesh.edgesOnVertex - 1 - - vertex_degree = ds_mesh.sizes['vertexDegree'] - - vert_integ_vorticity = xr.zeros_like(ds_mesh.latVertex) - for iedge in range(vertex_degree): - eov = edges_on_vertex.isel(vertexDegree=iedge) - edge_sign = edge_sign_on_vertex[:, iedge] - dc = dc_edge.isel(nEdges=eov) - vert_integ_vel = vert_integ_velocity.isel(nEdges=eov) - vert_integ_vorticity += ( - dc / area_vertex * edge_sign * vert_integ_vel) - - return vert_integ_vorticity - - def _compute_barotropic_streamfunction_vertex(self, ds_mesh, ds): - edge_sign_on_vertex = self._compute_edge_sign_on_vertex(ds_mesh) - vert_integ_velocity = self._compute_vert_integ_velocity(ds_mesh, ds) - vert_integ_vorticity = self._compute_vert_integ_vorticity( - ds_mesh, vert_integ_velocity, edge_sign_on_vertex) - self.logger.info('vertically integrated vorticity computed.') - - config = self.config - lat_range = config.getexpression( - 'climatologyMapBSF', 'latitudeRangeForZeroBSF') - - nvertices = ds_mesh.sizes['nVertices'] - vertex_degree = ds_mesh.sizes['vertexDegree'] - - cells_on_vertex = ds_mesh.cellsOnVertex - 1 - edges_on_vertex = ds_mesh.edgesOnVertex - 1 - vertices_on_edge = ds_mesh.verticesOnEdge - 1 - area_vertex = ds_mesh.areaTriangle - dc_edge = ds_mesh.dcEdge - dv_edge = ds_mesh.dvEdge - - # one equation involving vertex degree + 1 vertices for each vertex - # plus 2 entries for the boundary condition and Lagrange multiplier - ndata = (vertex_degree + 1) * nvertices + 2 - indices = np.zeros((2, ndata), dtype=int) - data = np.zeros(ndata, dtype=float) - - # the laplacian on the dual mesh of the streamfunction is the - # vertically integrated vorticity - vertices = np.arange(nvertices, dtype=int) - idata = (vertex_degree + 1) * vertices + 1 - indices[0, idata] = vertices - indices[1, idata] = vertices - for iedge in range(vertex_degree): - eov = edges_on_vertex.isel(vertexDegree=iedge) - dc = dc_edge.isel(nEdges=eov) - dv = dv_edge.isel(nEdges=eov) - - v0 = vertices_on_edge.isel(nEdges=eov, TWO=0) - v1 = vertices_on_edge.isel(nEdges=eov, TWO=1) - - edge_sign = edge_sign_on_vertex[:, iedge] - - mask = v0 == vertices - # the difference is v1 - v0, so we want to subtract this vertex - # when it is v0 and add it when it is v1 - this_vert_sign = np.where(mask, -1., 1.) - # the other vertex is obviously whichever one this is not - other_vert_index = np.where(mask, v1, v0) - # if there are invalid vertices, we need to make sure we don't - # index out of bounds. The edge_sign will mask these out - other_vert_index = np.where(other_vert_index >= 0, - other_vert_index, 0) - - idata_other = idata + iedge + 1 - - indices[0, idata] = vertices - indices[1, idata] = vertices - indices[0, idata_other] = vertices - indices[1, idata_other] = other_vert_index - - this_data = this_vert_sign * edge_sign * dc / (dv * area_vertex) - data[idata] += this_data - data[idata_other] = -this_data - - # Now, the boundary condition: To begin with, we set the BSF at the - # frist vertext to zero - indices[0, -2] = nvertices - indices[1, -2] = 0 - data[-2] = 1. - - # The same in the final column - indices[0, -1] = 0 - indices[1, -1] = nvertices - data[-1] = 1. - - # one extra spot for the Lagrange multiplier - rhs = np.zeros(nvertices + 1, dtype=float) - - rhs[0:-1] = vert_integ_vorticity.values - - matrix = scipy.sparse.csr_matrix( - (data, indices), - shape=(nvertices + 1, nvertices + 1)) - - solution = scipy.sparse.linalg.spsolve(matrix, rhs) - - # drop the Lagrange multiplier and convert to Sv with the desired sign - # convention - bsf_vertex = xr.DataArray(-1e-6 * solution[0:-1], - dims=('nVertices',)) - - bsf_vertex = _shift_bsf(bsf_vertex, lat_range, cells_on_vertex, - ds_mesh.latVertex) - - return bsf_vertex - - -def _shift_bsf(bsf_vertex, lat_range, cells_on_vertex, lat_vertex): - """ - Shift the barotropic streamfunction to be zero at the boundary over - the given latitude range - """ - is_boundary_cov = cells_on_vertex == -1 - boundary_vertices = is_boundary_cov.sum(dim='vertexDegree') > 0 - - boundary_vertices = np.logical_and( - boundary_vertices, - lat_vertex >= np.deg2rad(lat_range[0]) - ) - boundary_vertices = np.logical_and( - boundary_vertices, - lat_vertex <= np.deg2rad(lat_range[1]) - ) - - # convert from boolean mask to indices - boundary_vertices = np.flatnonzero(boundary_vertices.values) - - mean_boundary_bsf = bsf_vertex.isel(nVertices=boundary_vertices).mean() - - bsf_shifted = bsf_vertex - mean_boundary_bsf - - return bsf_shifted From 56f1e89eb53d5564b407b038616690e54d9641d8 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sun, 13 Apr 2025 12:11:14 -0500 Subject: [PATCH 020/116] Update to mpas_tools >=1.1.0 --- ci/recipe/meta.yaml | 2 +- dev-spec.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/recipe/meta.yaml b/ci/recipe/meta.yaml index bfd22bb0d..f1b412d0f 100644 --- a/ci/recipe/meta.yaml +++ b/ci/recipe/meta.yaml @@ -32,7 +32,7 @@ requirements: - lxml - mache >=1.11.0 - matplotlib-base >=3.9.0 - - mpas_tools >=1.0.0,<2.0.0 + - mpas_tools >=1.1.0,<2.0.0 - nco >=4.8.1,!=5.2.6 - netcdf4 - numpy >=2.0,<3.0 diff --git a/dev-spec.txt b/dev-spec.txt index 9f826e7bb..1ce19783b 100644 --- a/dev-spec.txt +++ b/dev-spec.txt @@ -15,7 +15,7 @@ gsw lxml mache >=1.11.0 matplotlib-base>=3.9.0 -mpas_tools >=1.0.0,<2.0.0 +mpas_tools >=1.1.0,<2.0.0 nco>=4.8.1,!=5.2.6 netcdf4 numpy>=2.0,<3.0 From e98122903abec97652f7c2c7510d7b4d1004ac35 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 15 Apr 2025 09:19:56 -0500 Subject: [PATCH 021/116] Reserve all processes for BSF calculation This seems to be necessary, at least at high res, to not run out of memory. --- mpas_analysis/ocean/climatology_map_bsf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mpas_analysis/ocean/climatology_map_bsf.py b/mpas_analysis/ocean/climatology_map_bsf.py index 5a55cf516..b385d99fe 100644 --- a/mpas_analysis/ocean/climatology_map_bsf.py +++ b/mpas_analysis/ocean/climatology_map_bsf.py @@ -216,6 +216,11 @@ def __init__(self, mpas_climatology_task, parent_task, variable_list, seasons, comparison_grid_names, subtaskName=subtask_name, vertices=True) + # this reequires a lot of memory so let's reserve all the available + # tasks + parallelTaskCount = self.config.getint('execute', 'parallelTaskCount') + self.subprocessCount = parallelTaskCount + self.min_depth = min_depth self.max_depth = max_depth self.include_bolus = None From 270ea87ba96aecb5c2dde2bb2ce9629140dceef8 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 16 Apr 2025 18:40:24 -0600 Subject: [PATCH 022/116] Expand variables for polar regions conservation and mass fluxes These are the variables we want to BlueTip, which seems like a good reason we should want them for polar regions generally (at least for now). --- mpas_analysis/polar_regions.cfg | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/mpas_analysis/polar_regions.cfg b/mpas_analysis/polar_regions.cfg index f6e4a9b1b..96f6550ca 100644 --- a/mpas_analysis/polar_regions.cfg +++ b/mpas_analysis/polar_regions.cfg @@ -806,7 +806,10 @@ transportGroups = ['Transport Transects', 'Arctic Transport Transects'] # land_ice_mass_change : Mass anomaly due to land ice fluxes # land_ice_ssh_change : SSH anomaly due to land ice fluxes # land_ice_mass_flux_components : Mass fluxes from land ice -plotTypes = ['absolute_energy_error', 'absolute_salt_error', 'total_mass_change', 'land_ice_mass_flux_components', 'land_ice_mass_change'] +plotTypes = ['absolute_energy_error', 'absolute_salt_error', + 'total_mass_change', 'land_ice_mass_change', + 'land_ice_ssh_change', 'land_ice_mass_flux', + 'land_ice_mass_flux_components'] [timeSeriesArcticOceanRegions] @@ -825,3 +828,10 @@ regionNames = ['all'] # See "regionNames" in the antarcticRegions masks file in # regionMaskSubdirectory for details. regionNames = ['all'] + + +[climatologyMapMassFluxes] + +variables = ['riverRunoffFlux', 'iceRunoffFlux', 'snowFlux', 'rainFlux', + 'evaporationFlux', 'seaIceFreshWaterFlux', + 'landIceFreshwaterFlux', 'icebergFreshWaterFlux'] From df305fbd3cb1674a2935305801b0e46b0b80a8b9 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 16 Apr 2025 20:27:09 -0500 Subject: [PATCH 023/116] Add `anomaly` tag to `timeSeriesOceanRegions` The tag ony gets added if there are regions where we will plot anomalies (i.e. OHC). --- mpas_analysis/ocean/time_series_ocean_regions.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mpas_analysis/ocean/time_series_ocean_regions.py b/mpas_analysis/ocean/time_series_ocean_regions.py index 95572eed7..b235f825f 100644 --- a/mpas_analysis/ocean/time_series_ocean_regions.py +++ b/mpas_analysis/ocean/time_series_ocean_regions.py @@ -106,11 +106,17 @@ def __init__(self, config, regionMasksTask, controlConfig=None): 'tDim': 'time', 'legend': 'WOA23 1991-2020 ANN mean'}} + anyAnomalies = False + for regionGroup in regionGroups: sectionSuffix = regionGroup[0].upper() + \ regionGroup[1:].replace(' ', '') sectionName = 'timeSeries{}'.format(sectionSuffix) + anomalyVars = config.getexpression(sectionName, 'anomalies') + if len(anomalyVars) > 0: + anyAnomalies = True + regionNames = config.getexpression(sectionName, 'regionNames') if len(regionNames) == 0: # no regions in this group were requested @@ -192,6 +198,9 @@ def __init__(self, config, regionMasksTask, controlConfig=None): plotRegionSubtask.run_after(combineSubtask) self.add_subtask(plotRegionSubtask) + if anyAnomalies: + self.tags.append('anomaly') + class ComputeRegionDepthMasksSubtask(AnalysisTask): """ From b57616205bad8d62e02054b8d6faac8436d6c1b8 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 16 Apr 2025 21:29:42 -0500 Subject: [PATCH 024/116] Put conservation time series in its own gallery group --- mpas_analysis/ocean/conservation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mpas_analysis/ocean/conservation.py b/mpas_analysis/ocean/conservation.py index 50c915a65..2b22a68d0 100644 --- a/mpas_analysis/ocean/conservation.py +++ b/mpas_analysis/ocean/conservation.py @@ -512,9 +512,8 @@ def _make_plot(self, plot_type): filePrefix=filePrefix, componentName='Ocean', componentSubdirectory='ocean', - galleryGroup='Time Series', - groupLink='timeseries', - gallery='Conservation', + galleryGroup='Conservation Time Series', + groupLink='conserv_timeseries', thumbnailDescription=title, imageDescription=caption, imageCaption=caption) From a31dbb7aaa79836562bf2a66c6c930fcd003f4a4 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 16 Apr 2025 22:31:07 -0500 Subject: [PATCH 025/116] Add anomaly tag to `hovmollerOceanRegions` ... if appropriate --- mpas_analysis/ocean/hovmoller_ocean_regions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mpas_analysis/ocean/hovmoller_ocean_regions.py b/mpas_analysis/ocean/hovmoller_ocean_regions.py index d971a2fff..59aec3609 100644 --- a/mpas_analysis/ocean/hovmoller_ocean_regions.py +++ b/mpas_analysis/ocean/hovmoller_ocean_regions.py @@ -76,6 +76,7 @@ def __init__(self, config, regionMasksTask, oceanRegionalProfilesTask, regionGroups = config.getexpression('hovmollerOceanRegions', 'regionGroups') + anyAnomalies = False for regionGroup in regionGroups: suffix = regionGroup[0].upper() + regionGroup[1:].replace(' ', '') @@ -87,6 +88,8 @@ def __init__(self, config, regionMasksTask, oceanRegionalProfilesTask, computeAnomaly = config.getboolean(regionGroupSection, 'computeAnomaly') + if computeAnomaly: + anyAnomalies = True fields = config.getexpression(regionGroupSection, 'fields') @@ -185,6 +188,8 @@ def __init__(self, config, regionMasksTask, oceanRegionalProfilesTask, self.add_subtask(hovmollerSubtask) self.run_after(oceanRegionalProfilesTask) + if anyAnomalies: + self.tags.append('anomaly') class ComputeHovmollerAnomalySubtask(AnalysisTask): From 3feb29b77a53f313d4fe60b9d6fa4e2efb0f6916 Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Fri, 18 Apr 2025 11:08:37 -0500 Subject: [PATCH 026/116] Switch to micromamba in build workflow --- .github/workflows/build_workflow.yml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_workflow.yml b/.github/workflows/build_workflow.yml index b62231bc3..e622b364c 100644 --- a/.github/workflows/build_workflow.yml +++ b/.github/workflows/build_workflow.yml @@ -53,21 +53,22 @@ jobs: - if: ${{ steps.skip_check.outputs.should_skip != 'true' }} name: Set up Conda Environment - uses: conda-incubator/setup-miniconda@v3 + uses: mamba-org/setup-micromamba@v2 with: - activate-environment: "mpas_analysis_ci" - miniforge-version: latest - channels: conda-forge - channel-priority: strict - auto-update-conda: false - python-version: ${{ matrix.python-version }} + environment-name: mpas_analysis_dev + init-shell: bash + condarc: | + channel_priority: strict + channels: + - conda-forge + create-args: >- + python=${{ matrix.python-version }} - if: ${{ steps.skip_check.outputs.should_skip != 'true' }} name: Install mpas_analysis run: | - conda create -n mpas_analysis_dev --file dev-spec.txt \ - python=${{ matrix.python-version }} - conda activate mpas_analysis_dev + conda install -y --file dev-spec.txt \ + python=${{ matrix.python-version }} python -m pip install --no-deps --no-build-isolation -vv -e . - if: ${{ steps.skip_check.outputs.should_skip != 'true' }} @@ -76,7 +77,6 @@ jobs: CHECK_IMAGES: False run: | set -e - conda activate mpas_analysis_dev pip check pytest --pyargs mpas_analysis mpas_analysis --help @@ -85,7 +85,6 @@ jobs: - if: ${{ steps.skip_check.outputs.should_skip != 'true' }} name: Build Sphinx Docs run: | - conda activate mpas_analysis_dev # sphinx-multiversion expects at least a "main" branch git branch main || echo "branch main already exists." cd docs From 4eccd27bea9a46ed85fdbabf1817e982b0c4ead0 Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Fri, 18 Apr 2025 16:39:20 -0500 Subject: [PATCH 027/116] Switch docs workflow to micromamba --- .github/workflows/docs_workflow.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/docs_workflow.yml b/.github/workflows/docs_workflow.yml index 832419107..27ffdd2da 100644 --- a/.github/workflows/docs_workflow.yml +++ b/.github/workflows/docs_workflow.yml @@ -37,28 +37,28 @@ jobs: - if: ${{ steps.skip_check.outputs.should_skip != 'true' }} name: Set up Conda Environment - uses: conda-incubator/setup-miniconda@v3 + uses: mamba-org/setup-micromamba@v2 with: - activate-environment: "mpas_analysis_ci" - miniforge-version: latest - channels: conda-forge - channel-priority: strict - auto-update-conda: false - python-version: ${{ env.PYTHON_VERSION }} + environment-name: mpas_analysis_dev + init-shell: bash + condarc: | + channel_priority: strict + channels: + - conda-forge + create-args: >- + python=${{ env.PYTHON_VERSION }} - if: ${{ steps.skip_check.outputs.should_skip != 'true' }} name: Install mpas_analysis run: | git config --global url."https://github.com/".insteadOf "git@github.com:" - conda create -n mpas_analysis_dev --file dev-spec.txt \ + conda install -y --file dev-spec.txt \ python=${{ env.PYTHON_VERSION }} - conda activate mpas_analysis_dev python -m pip install -vv --no-deps --no-build-isolation -e . - name: Build Sphinx Docs run: | set -e - conda activate mpas_analysis_dev pip check mpas_analysis sync diags --help cd docs @@ -66,7 +66,6 @@ jobs: - name: Copy Docs and Commit run: | set -e - conda activate mpas_analysis_dev pip check mpas_analysis sync diags --help cd docs From f3227b1584b12838a9103ebed8418486798e59a7 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 17 Feb 2025 06:16:42 -0600 Subject: [PATCH 028/116] Add support for transects of fields with nVertLevelsP1 These fields are at level interfaces, rather than level centers. --- .../ocean/compute_transects_subtask.py | 107 +++++++++++++----- 1 file changed, 80 insertions(+), 27 deletions(-) diff --git a/mpas_analysis/ocean/compute_transects_subtask.py b/mpas_analysis/ocean/compute_transects_subtask.py index b12ca7c9b..b2b7c8b45 100644 --- a/mpas_analysis/ocean/compute_transects_subtask.py +++ b/mpas_analysis/ocean/compute_transects_subtask.py @@ -35,7 +35,10 @@ make_directories from mpas_analysis.shared.io import write_netcdf_with_fill -from mpas_analysis.ocean.utility import compute_zmid +from mpas_analysis.ocean.utility import ( + compute_zinterface, + compute_zmid +) from mpas_analysis.shared.interpolation import interp_1d @@ -80,6 +83,11 @@ class ComputeTransectsSubtask(RemapMpasClimatologySubtask): zMid : ``xarray.DataArray`` Vertical coordinate at the center of layers, used to interpolate to reference depths + + zInterface : ``xarray.DataArray`` + Vertical coordinate at the interfaces between layers, used to + interpolate to reference depths + """ # Authors # ------- @@ -162,6 +170,7 @@ def __init__(self, mpasClimatologyTask, parentTask, climatologyName, self.collectionDescriptor = None self.maxLevelCell = None self.zMid = None + self.zInterface = None self.remap = self.obsDatasets.horizontalResolution != 'mpas' if self.obsDatasets.horizontalResolution == 'mpas' and \ self.verticalComparisonGridName != 'mpas': @@ -244,13 +253,21 @@ def run_task(self): self.maxLevelCell = dsMesh.maxLevelCell - 1 if self.remap: - zMid = compute_zmid(dsMesh.bottomDepth, dsMesh.maxLevelCell-1, + zMid = compute_zmid(dsMesh.bottomDepth, self.maxLevelCell, dsMesh.layerThickness) self.zMid = \ xr.DataArray.from_dict({'dims': ('nCells', 'nVertLevels'), 'data': zMid}) + zInterface = compute_zinterface(dsMesh.bottomDepth, + self.maxLevelCell, + dsMesh.layerThickness) + + self.zInterface = \ + xr.DataArray.from_dict({'dims': ('nCells', 'nVertLevelsP1'), + 'data': zInterface}) + # then, call run from the base class (RemapMpasClimatologySubtask), # which will perform masking and possibly horizontal remapping super(ComputeTransectsSubtask, self).run_task() @@ -308,20 +325,35 @@ def customize_masked_climatology(self, climatology, season): # ------- # Xylar Asay-Davis - zIndex = xr.DataArray.from_dict( - {'dims': ('nVertLevels',), - 'data': numpy.arange(climatology.sizes['nVertLevels'])}) + maxLevel = { + 'nVertLevels': self.maxLevelCell, + 'nVertLevelsP1': self.maxLevelCell + 1 + } + + for vertDim in ['nVertLevels', 'nVertLevelsP1']: + if vertDim in climatology.dims: + zIndex = xr.DataArray.from_dict( + {'dims': (vertDim,), + 'data': numpy.arange(climatology.sizes[vertDim])}) - cellMask = zIndex <= self.maxLevelCell + mask = zIndex <= maxLevel[vertDim] - for variableName in self.variableList: - climatology[variableName] = \ - climatology[variableName].where(cellMask) + for variableName in self.variableList: + if vertDim in climatology[variableName].dims: + climatology[variableName] = \ + climatology[variableName].where(mask) if self.remap: - climatology['zMid'] = self.zMid + if 'nVertLevels' in climatology.dims: + climatology['zMid'] = self.zMid + if 'nVertLevelsP1' in climatology.dims: + climatology['zInterface'] = self.zInterface + + transposeDims = ['nVertLevels', 'nVertLevelsP1', 'nCells'] + transposeDims = [dim for dim in transposeDims if dim in + climatology.dims] - climatology = climatology.transpose('nVertLevels', 'nCells') + climatology = climatology.transpose(*transposeDims) return climatology @@ -358,10 +390,11 @@ def customize_remapped_climatology(self, climatology, comparisonGridNames, if 'nCells' in climatology.dims: climatology = climatology.rename({'nCells': 'nPoints'}) - dims = ['nPoints', 'nVertLevels'] - if 'nv' in climatology.dims: - dims.append('nv') - climatology = climatology.transpose(*dims) + transposeDims = ['nPoints', 'nVertLevels', 'nVertLevelsP1', 'nv'] + transposeDims = [dim for dim in transposeDims if dim in + climatology.dims] + + climatology = climatology.transpose(*transposeDims) return climatology @@ -399,29 +432,46 @@ def _vertical_interp(self, ds, transectIndex, dsObs, outFileName, ds = ds.where(ds.transectNumber == transectIndex, drop=True) if self.verticalComparisonGridName == 'mpas': - z = ds.zMid - z = z.rename({'nVertLevels': 'nzOut'}) + z = ds['zMid'] + ds.rename({'nVertLevels': 'nzOut'}) elif self.verticalComparisonGridName == 'obs': z = dsObs.z z = z.rename({'nz': 'nzOut'}) else: # a defined vertical grid z = (('nzOut', ), self.verticalComparisonGrid) + ds['z'] = z + + for vertDim, vertCoord in ( + ('nVertLevels', 'zMid'), ('nVertLevelsP1', 'zInterface')): + if vertDim not in ds.dims: + continue + + if self.verticalComparisonGridName == 'mpas' and \ + vertDim == 'nVertLevels': + # no interpolation needed + continue - if self.verticalComparisonGridName == 'mpas': - ds = ds.rename({'zMid': 'z', 'nVertLevels': 'nz'}) - else: - ds['z'] = z # remap each variable - ds = interp_1d(ds, inInterpDim='nVertLevels', inInterpCoord='zMid', - outInterpDim='nzOut', outInterpCoord='z') - ds = ds.rename({'nzOut': 'nz'}) + ds = interp_1d( + ds, + inInterpDim=vertDim, + inInterpCoord=vertCoord, + outInterpDim='nzOut', + outInterpCoord='z', + ) + ds = ds.rename({'nzOut': 'nz'}) if self.verticalComparisonGridName != 'obs' and 'nz' in dsObs.dims: dsObs['zOut'] = z # remap each variable - dsObs = interp_1d(dsObs, inInterpDim='nz', inInterpCoord='z', - outInterpDim='nzOut', outInterpCoord='zOut') + dsObs = interp_1d( + dsObs, + inInterpDim='nz', + inInterpCoord='z', + outInterpDim='nzOut', + outInterpCoord='zOut', + ) dsObs = dsObs.rename({'nzOut': 'nz'}) write_netcdf_with_fill(dsObs, outObsFileName) @@ -561,7 +611,9 @@ def _compute_mpas_transects(self, dsMesh): dsOnMpas = xr.Dataset(dsMpasTransect) for var in dsMask.data_vars: dims = dsMask[var].dims - if 'nCells' in dims and 'nVertLevels' in dims: + if 'nCells' in dims and ( + 'nVertLevels' in dims or + 'nVertLevelsP1' in dims): dsOnMpas[var] = \ interp_mpas_to_transect_nodes( dsMpasTransect, dsMask[var]) @@ -611,6 +663,7 @@ def _transpose(dsOnMpas): class TransectsObservations(object): + """ A class for loading and manipulating transect observations From 703fc975f4abee1098b0094f8b1eb4c4b89f3c79 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 17 Feb 2025 06:17:35 -0600 Subject: [PATCH 029/116] Update geojson transects to include vert. vel., diff. and visc. These are examples of fields at level interfaces. This merge also adjusts details (colorbar range and removes contours) for meridional and zonal velocity plots of geojson transects. --- mpas_analysis/default.cfg | 124 +++++++++++++++++++++++++++++++++++--- 1 file changed, 115 insertions(+), 9 deletions(-) diff --git a/mpas_analysis/default.cfg b/mpas_analysis/default.cfg index abf55fe27..16d862b14 100755 --- a/mpas_analysis/default.cfg +++ b/mpas_analysis/default.cfg @@ -3660,7 +3660,20 @@ fields = {'prefix': 'meridionalVelocity', 'mpas': 'timeMonthly_avg_velocityMeridional', 'units': r'm s$$^{-1}$$', - 'titleName': 'Meridional Velocity'}] + 'titleName': 'Meridional Velocity'}, + {'prefix': 'vertVelocity', + 'mpas': 'timeMonthly_avg_vertVelocityTop', + 'units': r'm s$$^{-1}$$', + 'titleName': 'Vertical Velocity'}, + {'prefix': 'vertDiff', + 'mpas': 'timeMonthly_avg_vertDiffTopOfCell', + 'units': r'm s$$^{-1}$$', + 'titleName': 'Vertical Diffusivity'}, + {'prefix': 'vertVisc', + 'mpas': 'timeMonthly_avg_vertViscTopOfCell', + 'units': r'm s$$^{-1}$$', + 'titleName': 'Vertical Viscosity'}, + ] # Times for comparison times (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, # Nov, Dec, JFM, AMJ, JAS, OND, ANN) @@ -3800,12 +3813,12 @@ colormapTypeResult = continuous # the type of norm used in the colormap normTypeResult = linear # A dictionary with keywords for the norm -normArgsResult = {'vmin': -0.2, 'vmax': 0.2} +normArgsResult = {'vmin': -0.05, 'vmax': 0.05} # determine the ticks automatically by default, uncomment to specify # colorbarTicksResult = numpy.linspace(-0.2, 0.2, 9) # contour line levels (use [] for automatic contour selection, 'none' for no # contour lines) -contourLevelsResult = [] +contourLevelsResult = 'none' # colormap for differences colormapNameDifference = balance @@ -3814,12 +3827,12 @@ colormapTypeDifference = continuous # the type of norm used in the colormap normTypeDifference = linear # A dictionary with keywords for the norm -normArgsDifference = {'vmin': -0.2, 'vmax': 0.2} +normArgsDifference = {'vmin': -0.05, 'vmax': 0.05} # determine the ticks automatically by default, uncomment to specify # colorbarTicksDifference = numpy.linspace(-0.2, 0.2, 9) # contour line levels (use [] for automatic contour selection, 'none' for no # contour lines) -contourLevelsDifference = [] +contourLevelsDifference = 'none' [geojsonMeridionalVelocityTransects] @@ -3832,12 +3845,12 @@ colormapTypeResult = continuous # the type of norm used in the colormap normTypeResult = linear # A dictionary with keywords for the norm -normArgsResult = {'vmin': -0.2, 'vmax': 0.2} +normArgsResult = {'vmin': -0.05, 'vmax': 0.05} # determine the ticks automatically by default, uncomment to specify # colorbarTicksResult = numpy.linspace(-0.2, 0.2, 9) # contour line levels (use [] for automatic contour selection, 'none' for no # contour lines) -contourLevelsResult = [] +contourLevelsResult = 'none' # colormap for differences colormapNameDifference = balance @@ -3846,12 +3859,105 @@ colormapTypeDifference = continuous # the type of norm used in the colormap normTypeDifference = linear # A dictionary with keywords for the norm -normArgsDifference = {'vmin': -0.2, 'vmax': 0.2} +normArgsDifference = {'vmin': -0.05, 'vmax': 0.05} # determine the ticks automatically by default, uncomment to specify # colorbarTicksDifference = numpy.linspace(-0.2, 0.2, 9) # contour line levels (use [] for automatic contour selection, 'none' for no # contour lines) -contourLevelsDifference = [] +contourLevelsDifference = 'none' + +[geojsonVertVelocityTransects] +## options related to plotting geojson transects of meridional velocity + +# colormap for model/observations +colormapNameResult = delta +# whether the colormap is indexed or continuous +colormapTypeResult = continuous +# the type of norm used in the colormap +normTypeResult = linear +# A dictionary with keywords for the norm +normArgsResult = {'vmin': -1e-5, 'vmax': 1e-5} +# determine the ticks automatically by default, uncomment to specify +# colorbarTicksResult = numpy.linspace(-0.2, 0.2, 9) +# contour line levels (use [] for automatic contour selection, 'none' for no +# contour lines) +contourLevelsResult = 'none' + +# colormap for differences +colormapNameDifference = balance +# whether the colormap is indexed or continuous +colormapTypeDifference = continuous +# the type of norm used in the colormap +normTypeDifference = linear +# A dictionary with keywords for the norm +normArgsDifference = {'vmin': -1e-5, 'vmax': 1e-5} +# determine the ticks automatically by default, uncomment to specify +# colorbarTicksDifference = numpy.linspace(-0.2, 0.2, 9) +# contour line levels (use [] for automatic contour selection, 'none' for no +# contour lines) +contourLevelsDifference = 'none' + +[geojsonVertDiffTransects] +## options related to plotting geojson transects of meridional velocity + +# colormap for model/observations +colormapNameResult = rain +# whether the colormap is indexed or continuous +colormapTypeResult = continuous +# the type of norm used in the colormap +normTypeResult = log +# A dictionary with keywords for the norm +normArgsResult = {'vmin': 1e-6, 'vmax': 1.} +# determine the ticks automatically by default, uncomment to specify +# colorbarTicksResult = numpy.linspace(-0.2, 0.2, 9) +# contour line levels (use [] for automatic contour selection, 'none' for no +# contour lines) +contourLevelsResult = 'none' + +# colormap for differences +colormapNameDifference = balance +# whether the colormap is indexed or continuous +colormapTypeDifference = continuous +# the type of norm used in the colormap +normTypeDifference = linear +# A dictionary with keywords for the norm +normArgsDifference = {'vmin': -0.5, 'vmax': 0.5} +# determine the ticks automatically by default, uncomment to specify +# colorbarTicksDifference = numpy.linspace(-0.2, 0.2, 9) +# contour line levels (use [] for automatic contour selection, 'none' for no +# contour lines) +contourLevelsDifference = 'none' + +[geojsonVertViscTransects] +## options related to plotting geojson transects of meridional velocity + +# colormap for model/observations +colormapNameResult = rain +# whether the colormap is indexed or continuous +colormapTypeResult = continuous +# the type of norm used in the colormap +normTypeResult = log +# A dictionary with keywords for the norm +normArgsResult = {'vmin': 1e-6, 'vmax': 1.} +# determine the ticks automatically by default, uncomment to specify +# colorbarTicksResult = numpy.linspace(-0.2, 0.2, 9) +# contour line levels (use [] for automatic contour selection, 'none' for no +# contour lines) +contourLevelsResult = 'none' + +# colormap for differences +colormapNameDifference = balance +# whether the colormap is indexed or continuous +colormapTypeDifference = continuous +# the type of norm used in the colormap +normTypeDifference = linear +# A dictionary with keywords for the norm +normArgsDifference = {'vmin': -0.5, 'vmax': 0.5} +# determine the ticks automatically by default, uncomment to specify +# colorbarTicksDifference = numpy.linspace(-0.2, 0.2, 9) +# contour line levels (use [] for automatic contour selection, 'none' for no +# contour lines) +contourLevelsDifference = 'none' [soseTransects] From 7da046726ba26d0918b830a26e6e5a14838c3594 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 17 Feb 2025 07:26:52 -0600 Subject: [PATCH 030/116] Add support for NetCDF transects without observations This is accommodated by adding NetCDF support to the geojson transect task. --- mpas_analysis/__main__.py | 6 +- mpas_analysis/default.cfg | 42 ++- mpas_analysis/ocean/__init__.py | 4 +- .../ocean/geojson_netcdf_transects.py | 328 ++++++++++++++++++ mpas_analysis/ocean/geojson_transects.py | 223 ------------ 5 files changed, 360 insertions(+), 243 deletions(-) create mode 100644 mpas_analysis/ocean/geojson_netcdf_transects.py delete mode 100644 mpas_analysis/ocean/geojson_transects.py diff --git a/mpas_analysis/__main__.py b/mpas_analysis/__main__.py index ea00d46e0..7ffcaef30 100644 --- a/mpas_analysis/__main__.py +++ b/mpas_analysis/__main__.py @@ -244,9 +244,9 @@ def build_analysis_list(config, controlConfig): analyses.append(ocean.WoaTransects(config, oceanClimatologyTasks['avg'], controlConfig)) - analyses.append(ocean.GeojsonTransects(config, - oceanClimatologyTasks['avg'], - controlConfig)) + analyses.append(ocean.GeojsonNetcdfTransects(config, + oceanClimatologyTasks['avg'], + controlConfig)) oceanRegionalProfiles = ocean.OceanRegionalProfiles( config, oceanRegionMasksTask, controlConfig) diff --git a/mpas_analysis/default.cfg b/mpas_analysis/default.cfg index 16d862b14..cc7175a47 100755 --- a/mpas_analysis/default.cfg +++ b/mpas_analysis/default.cfg @@ -3616,9 +3616,11 @@ normArgsDifference = {'vmin': -1.0, 'vmax': 1.0} contourLevelsDifference = np.arange(-0.9, 1.0, 0.4) -[geojsonTransects] +[geojsonNetcdfTransects] ## options related to plotting model transects at points determined by a -## geojson file. To generate your own geojson file, go to: +## user-specified geojson or NetCDF file. +## +## To generate your own geojson file, go to: ## http://geojson.io/ ## and draw one or more polylines, then add a name to each: ## @@ -3629,12 +3631,20 @@ contourLevelsDifference = np.arange(-0.9, 1.0, 0.4) ## option: ## geojsonFiles = ['transects.geojson'] ## (giving an absolute path if necessary) in your custom config file. - -# a list of geojson files containing lat/lon points in LineStrings to be -# plotted. If relative paths are given, they are relative to the current -# working directory. The files must be listed in quotes, e.g.: -# geojsonFiles = ['file1.geojson', '/path/to/file2.geojson'] -geojsonFiles = [] +## +## If you provide a NetCDF file instead, it simply needs to have 'lat` and +## `lon` variables. The `lat` and `lon` variables should be 1D arrays +## with the same dimension name (e.g. 'nPoints'). The name of the file +## (without the base path or extension) will serve as the transect name with +## underscores converted to spaces. + +# a list of geojson and/or NetCDF files. The geojson files must contain +# lat/lon points in LineStrings to be plotted. The NetCDF files need 'lat' +# and 'lon' variables with the same dimesion name. If relative paths are +# given, they are relative to the current working directory. The files must +# be listed in quotes, e.g.: +# geojsonOrNetcdfFiles = ['file1.geojson', '/path/to/file2.geojson', 'file3.nc'] +geojsonOrNetcdfFiles = [] # a list of dictionaries for each field to plot. The dictionary includes # prefix (used for file names, task names and sections) as well as the mpas @@ -3707,7 +3717,7 @@ verticalBounds = [] renormalizationThreshold = 0.01 -[geojsonTemperatureTransects] +[geojsonNetcdfTemperatureTransects] ## options related to plotting geojson transects of potential temperature # colormap for model/observations @@ -3739,7 +3749,7 @@ normArgsDifference = {'vmin': -2., 'vmax': 2.} contourLevelsDifference = [] -[geojsonSalinityTransects] +[geojsonNetcdfSalinityTransects] ## options related to plotting geojson transects of salinity # colormap for model/observations @@ -3771,7 +3781,7 @@ normArgsDifference = {'vmin': -0.5, 'vmax': 0.5} contourLevelsDifference = [] -[geojsonPotentialDensityTransects] +[geojsonNetcdfPotentialDensityTransects] ## options related to plotting geojson transects of potential density # colormap for model/observations @@ -3803,7 +3813,7 @@ normArgsDifference = {'vmin': -0.3, 'vmax': 0.3} contourLevelsDifference = [] -[geojsonZonalVelocityTransects] +[geojsonNetcdfZonalVelocityTransects] ## options related to plotting geojson transects of zonal velocity # colormap for model/observations @@ -3835,7 +3845,7 @@ normArgsDifference = {'vmin': -0.05, 'vmax': 0.05} contourLevelsDifference = 'none' -[geojsonMeridionalVelocityTransects] +[geojsonNetcdfMeridionalVelocityTransects] ## options related to plotting geojson transects of meridional velocity # colormap for model/observations @@ -3866,7 +3876,7 @@ normArgsDifference = {'vmin': -0.05, 'vmax': 0.05} # contour lines) contourLevelsDifference = 'none' -[geojsonVertVelocityTransects] +[geojsonNetcdfVertVelocityTransects] ## options related to plotting geojson transects of meridional velocity # colormap for model/observations @@ -3897,7 +3907,7 @@ normArgsDifference = {'vmin': -1e-5, 'vmax': 1e-5} # contour lines) contourLevelsDifference = 'none' -[geojsonVertDiffTransects] +[geojsonNetcdfVertDiffTransects] ## options related to plotting geojson transects of meridional velocity # colormap for model/observations @@ -3928,7 +3938,7 @@ normArgsDifference = {'vmin': -0.5, 'vmax': 0.5} # contour lines) contourLevelsDifference = 'none' -[geojsonVertViscTransects] +[geojsonNetcdfVertViscTransects] ## options related to plotting geojson transects of meridional velocity # colormap for model/observations diff --git a/mpas_analysis/ocean/__init__.py b/mpas_analysis/ocean/__init__.py index c99feb4b4..a64f2f212 100644 --- a/mpas_analysis/ocean/__init__.py +++ b/mpas_analysis/ocean/__init__.py @@ -61,7 +61,9 @@ from mpas_analysis.ocean.osnap_transects import OsnapTransects from mpas_analysis.ocean.sose_transects import SoseTransects from mpas_analysis.ocean.woa_transects import WoaTransects -from mpas_analysis.ocean.geojson_transects import GeojsonTransects +from mpas_analysis.ocean.geojson_netcdf_transects import ( + GeojsonNetcdfTransects +) from mpas_analysis.ocean.ocean_regional_profiles import \ OceanRegionalProfiles diff --git a/mpas_analysis/ocean/geojson_netcdf_transects.py b/mpas_analysis/ocean/geojson_netcdf_transects.py new file mode 100644 index 000000000..18c4fe4f7 --- /dev/null +++ b/mpas_analysis/ocean/geojson_netcdf_transects.py @@ -0,0 +1,328 @@ +# This software is open source software available under the BSD-3 license. +# +# Copyright (c) 2022 Triad National Security, LLC. All rights reserved. +# Copyright (c) 2022 Lawrence Livermore National Security, LLC. All rights +# reserved. +# Copyright (c) 2022 UT-Battelle, LLC. All rights reserved. +# +# Additional copyright and license information can be found in the LICENSE file +# distributed with this code, or at +# https://raw.githubusercontent.com/MPAS-Dev/MPAS-Analysis/main/LICENSE +import json +from pathlib import Path + +import numpy as np +import xarray as xr + +from mpas_analysis.ocean.compute_transects_subtask import ( + ComputeTransectsSubtask, + TransectsObservations +) +from mpas_analysis.ocean.plot_transect_subtask import PlotTransectSubtask +from mpas_analysis.shared import AnalysisTask + + +class GeojsonNetcdfTransects(AnalysisTask): + """ + Plot model output at transects defined by lat/lon points in a geojson or + NetCDF file + """ + # Authors + # ------- + # Xylar Asay-Davis + + def __init__(self, config, mpasClimatologyTask, controlConfig=None): + """ + Construct the analysis task and adds it as a subtask of the + ``parentTask``. + + Parameters + ---------- + config : mpas_tools.config.MpasConfigParser + Configuration options + + mpasClimatologyTask : ``MpasClimatologyTask`` + The task that produced the climatology to be remapped and plotted + as a transect + + controlconfig : mpas_tools.config.MpasConfigParser, optional + Configuration options for a control run (if any) + """ + # Authors + # ------- + # Xylar Asay-Davis + + tags = ['climatology', 'transect', 'geojson', 'netcdf'] + + # call the constructor from the base class (AnalysisTask) + super().__init__( + config=config, taskName='geojsonNetcdfTransects', + componentName='ocean', + tags=tags) + + sectionName = self.taskName + + geojsonOrNetcdfFiles = config.getexpression(sectionName, + 'geojsonOrNetcdfFiles') + if len(geojsonOrNetcdfFiles) == 0: + return + + seasons = config.getexpression(sectionName, 'seasons') + + horizontalResolution = config.get(sectionName, 'horizontalResolution') + + verticalComparisonGridName = config.get(sectionName, + 'verticalComparisonGridName') + + if verticalComparisonGridName in ['mpas', 'obs']: + verticalComparisonGrid = None + else: + verticalComparisonGrid = config.getexpression( + sectionName, 'verticalComparisonGrid', use_numpyfunc=True) + + fields = config.getexpression(sectionName, 'fields') + + geojsonFileNames = {} + netcdfFileNames = {} + transectNames = [] + for fileName in geojsonOrNetcdfFiles: + ext = Path(fileName).suffix + if ext == '.nc': + transectName = Path(fileName).stem + netcdfFileNames[transectName] = fileName + elif ext == '.geojson': + with open(fileName) as filePointer: + jsonFile = json.load(filePointer) + + for feature in jsonFile['features']: + if feature['geometry']['type'] != 'LineString': + continue + transectName = feature['properties']['name'] + + geojsonFileNames[transectName] = fileName + else: + raise ValueError(f'Unexptect file extension: {ext}') + + if transectName in transectNames: + raise ValueError(f'Transect name {transectName} is repeated.') + + transectNames.append(transectName) + + variableList = [field['mpas'] for field in fields] + + computeGeojsonTransectsSubtask = None + computeNetcdfTransectsSubtask = None + + if geojsonFileNames: + transectCollectionName = 'geojson_transects' + if horizontalResolution != 'obs' and \ + horizontalResolution != 'mpas': + transectCollectionName = \ + f'{transectCollectionName}_{horizontalResolution}km' + + geojsonObservations = GeojsonTransectsObservations( + config, geojsonFileNames, horizontalResolution, + transectCollectionName) + + computeGeojsonTransectsSubtask = ComputeTransectsSubtask( + mpasClimatologyTask=mpasClimatologyTask, + parentTask=self, + climatologyName='geojson', + transectCollectionName=transectCollectionName, + variableList=variableList, + seasons=seasons, + obsDatasets=geojsonObservations, + verticalComparisonGridName=verticalComparisonGridName, + verticalComparisonGrid=verticalComparisonGrid, + subtaskName='remapGeojson') + + if netcdfFileNames: + transectCollectionName = 'netcdf_transects' + if horizontalResolution != 'obs' and \ + horizontalResolution != 'mpas': + transectCollectionName = \ + f'{transectCollectionName}_{horizontalResolution}km' + + netcdfObservations = NetcdfTransectsObservations( + config, netcdfFileNames, horizontalResolution, + transectCollectionName) + + computeNetcdfTransectsSubtask = ComputeTransectsSubtask( + mpasClimatologyTask=mpasClimatologyTask, + parentTask=self, + climatologyName='netcdf', + transectCollectionName=transectCollectionName, + variableList=variableList, + seasons=seasons, + obsDatasets=netcdfObservations, + verticalComparisonGridName=verticalComparisonGridName, + verticalComparisonGrid=verticalComparisonGrid, + subtaskName='remapNetcdf') + + for field in fields: + for transectName in geojsonFileNames: + for season in seasons: + self._add_plot_subtasks( + field=field, season=season, + transectName=transectName, + computeSubtask=computeGeojsonTransectsSubtask, + galleryGroup='Geojson Transects', + groupLink='geojson', + controlConfig=controlConfig) + + for transectName in netcdfFileNames: + for season in seasons: + self._add_plot_subtasks( + field=field, season=season, + transectName=transectName, + computeSubtask=computeNetcdfTransectsSubtask, + galleryGroup='NetCDF Transects', + groupLink='nctransects', + controlConfig=controlConfig) + + def _add_plot_subtasks(self, field, season, transectName, computeSubtask, + galleryGroup, groupLink, controlConfig): + """ + Add a sbutask for plotting the given transect, field and season + """ + config = self.config + sectionName = self.taskName + verticalBounds = config.getexpression(sectionName, 'verticalBounds') + + fieldPrefix = field['prefix'] + + outFileLabel = fieldPrefix + + if controlConfig is None: + refFieldName = None + refTitleLabel = None + diffTitleLabel = None + else: + refFieldName = field['mpas'] + controlRunName = controlConfig.get('runs', 'mainRunName') + refTitleLabel = f'Control: {controlRunName}' + diffTitleLabel = 'Main - Control' + + fieldPrefixUpper = fieldPrefix[0].upper() + fieldPrefix[1:] + fieldNameInTitle = field['titleName'] + transectNameInTitle = transectName.replace('_', ' ') + fieldNameInTitle = \ + f'{fieldNameInTitle} from {transectNameInTitle}' + + configSectionName = f'geojsonNetcdf{fieldPrefixUpper}Transects' + + # make a new subtask for this season and comparison grid + subtask = PlotTransectSubtask( + parentTask=self, + season=season, + transectName=transectName, + fieldName=fieldPrefix, + computeTransectsSubtask=computeSubtask, + plotObs=False, + controlConfig=controlConfig) + + subtask.set_plot_info( + outFileLabel=outFileLabel, + fieldNameInTitle=fieldNameInTitle, + mpasFieldName=field['mpas'], + refFieldName=refFieldName, + refTitleLabel=refTitleLabel, + diffTitleLabel=diffTitleLabel, + unitsLabel=field['units'], + imageCaption=fieldNameInTitle, + galleryGroup=galleryGroup, + groupSubtitle=None, + groupLink=groupLink, + galleryName=field['titleName'], + configSectionName=configSectionName, + verticalBounds=verticalBounds) + + self.add_subtask(subtask) + + +class GeojsonTransectsObservations(TransectsObservations): + """ + A class for loading and manipulating geojson transects + """ + # Authors + # ------- + # Xylar Asay-Davis + + def build_observational_dataset(self, fileName, transectName): + """ + read in the data sets for observations, and possibly rename some + variables and dimensions + + Parameters + ---------- + fileName : str + observation file name + + transectName : str + transect name + + Returns + ------- + dsObs : ``xarray.Dataset`` + The observational dataset + """ + # Authors + # ------- + # Xylar Asay-Davis + + with open(fileName) as filePointer: + jsonFile = json.load(filePointer) + + for feature in jsonFile['features']: + if feature['properties']['name'] != transectName: + continue + assert feature['geometry']['type'] == 'LineString' + + coordinates = feature['geometry']['coordinates'] + lon, lat = zip(*coordinates) + break + + dsObs = xr.Dataset() + dsObs['lon'] = (('nPoints',), np.array(lon)) + dsObs.lon.attrs['units'] = 'degrees' + dsObs['lat'] = (('nPoints',), np.array(lat)) + dsObs.lat.attrs['units'] = 'degrees' + + return dsObs + + +class NetcdfTransectsObservations(TransectsObservations): + """ + A class for loading and manipulating netcdf transects + """ + # Authors + # ------- + # Xylar Asay-Davis + + def build_observational_dataset(self, fileName, transectName): + """ + read in the data sets for observations, and possibly rename some + variables and dimensions + + Parameters + ---------- + fileName : str + observation file name + + transectName : str + transect name + + Returns + ------- + dsObs : ``xarray.Dataset`` + The observational dataset + """ + # Authors + # ------- + # Xylar Asay-Davis + + dsObs = xr.open_dataset(fileName) + # drop all variables besides lon and lat + dsObs = dsObs[['lon', 'lat']] + + return dsObs diff --git a/mpas_analysis/ocean/geojson_transects.py b/mpas_analysis/ocean/geojson_transects.py deleted file mode 100644 index df62d3512..000000000 --- a/mpas_analysis/ocean/geojson_transects.py +++ /dev/null @@ -1,223 +0,0 @@ -# This software is open source software available under the BSD-3 license. -# -# Copyright (c) 2022 Triad National Security, LLC. All rights reserved. -# Copyright (c) 2022 Lawrence Livermore National Security, LLC. All rights -# reserved. -# Copyright (c) 2022 UT-Battelle, LLC. All rights reserved. -# -# Additional copyright and license information can be found in the LICENSE file -# distributed with this code, or at -# https://raw.githubusercontent.com/MPAS-Dev/MPAS-Analysis/main/LICENSE -from collections import OrderedDict -import json -import xarray -import numpy - -from mpas_analysis.shared import AnalysisTask -from mpas_analysis.ocean.compute_transects_subtask import \ - ComputeTransectsSubtask, TransectsObservations - -from mpas_analysis.ocean.plot_transect_subtask import PlotTransectSubtask - - -class GeojsonTransects(AnalysisTask): - """ - Plot model output at transects defined by lat/lon points in a geojson file - """ - # Authors - # ------- - # Xylar Asay-Davis - - def __init__(self, config, mpasClimatologyTask, controlConfig=None): - """ - Construct the analysis task and adds it as a subtask of the - ``parentTask``. - - Parameters - ---------- - config : mpas_tools.config.MpasConfigParser - Configuration options - - mpasClimatologyTask : ``MpasClimatologyTask`` - The task that produced the climatology to be remapped and plotted - as a transect - - controlconfig : mpas_tools.config.MpasConfigParser, optional - Configuration options for a control run (if any) - """ - # Authors - # ------- - # Xylar Asay-Davis - - tags = ['climatology', 'transect', 'geojson'] - - # call the constructor from the base class (AnalysisTask) - super(GeojsonTransects, self).__init__( - config=config, taskName='geojsonTransects', - componentName='ocean', - tags=tags) - - sectionName = self.taskName - - geojsonFiles = config.getexpression(sectionName, 'geojsonFiles') - if len(geojsonFiles) == 0: - return - - seasons = config.getexpression(sectionName, 'seasons') - - horizontalResolution = config.get(sectionName, 'horizontalResolution') - - verticalComparisonGridName = config.get(sectionName, - 'verticalComparisonGridName') - - if verticalComparisonGridName in ['mpas', 'obs']: - verticalComparisonGrid = None - else: - verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', use_numpyfunc=True) - - verticalBounds = config.getexpression(sectionName, 'verticalBounds') - - fields = config.getexpression(sectionName, 'fields') - - obsFileNames = OrderedDict() - for fileName in geojsonFiles: - with open(fileName) as filePointer: - jsonFile = json.load(filePointer) - - for feature in jsonFile['features']: - if feature['geometry']['type'] != 'LineString': - continue - transectName = feature['properties']['name'] - - obsFileNames[transectName] = fileName - - transectCollectionName = 'geojson_transects' - if horizontalResolution != 'obs': - transectCollectionName = '{}_{}km'.format(transectCollectionName, - horizontalResolution) - - transectsObservations = GeojsonTransectsObservations( - config, obsFileNames, horizontalResolution, - transectCollectionName) - - computeTransectsSubtask = ComputeTransectsSubtask( - mpasClimatologyTask=mpasClimatologyTask, - parentTask=self, - climatologyName='geojson', - transectCollectionName=transectCollectionName, - variableList=[field['mpas'] for field in fields], - seasons=seasons, - obsDatasets=transectsObservations, - verticalComparisonGridName=verticalComparisonGridName, - verticalComparisonGrid=verticalComparisonGrid) - - plotObs = False - if controlConfig is None: - - refTitleLabel = None - - diffTitleLabel = None - - else: - controlRunName = controlConfig.get('runs', 'mainRunName') - refTitleLabel = 'Control: {}'.format(controlRunName) - - diffTitleLabel = 'Main - Control' - - for field in fields: - fieldPrefix = field['prefix'] - for transectName in obsFileNames: - for season in seasons: - outFileLabel = fieldPrefix - if controlConfig is None: - refFieldName = None - else: - refFieldName = field['mpas'] - - fieldPrefixUpper = fieldPrefix[0].upper() + fieldPrefix[1:] - fieldNameInTytle = '{} from {}'.format( - field['titleName'], - transectName.replace('_', ' ')) - - # make a new subtask for this season and comparison grid - subtask = PlotTransectSubtask(self, season, transectName, - fieldPrefix, - computeTransectsSubtask, - plotObs, controlConfig) - - subtask.set_plot_info( - outFileLabel=outFileLabel, - fieldNameInTitle=fieldNameInTytle, - mpasFieldName=field['mpas'], - refFieldName=refFieldName, - refTitleLabel=refTitleLabel, - diffTitleLabel=diffTitleLabel, - unitsLabel=field['units'], - imageCaption=fieldNameInTytle, - galleryGroup='Geojson Transects', - groupSubtitle=None, - groupLink='geojson', - galleryName=field['titleName'], - configSectionName='geojson{}Transects'.format( - fieldPrefixUpper), - verticalBounds=verticalBounds) - - self.add_subtask(subtask) - - -class GeojsonTransectsObservations(TransectsObservations): - """ - A class for loading and manipulating geojson transects - - Attributes - ---------- - - obsDatasets : OrderedDict - A dictionary of observational datasets - """ - # Authors - # ------- - # Xylar Asay-Davis - - def build_observational_dataset(self, fileName, transectName): - """ - read in the data sets for observations, and possibly rename some - variables and dimensions - - Parameters - ---------- - fileName : str - observation file name - - transectName : str - transect name - - Returns - ------- - dsObs : ``xarray.Dataset`` - The observational dataset - """ - # Authors - # ------- - # Xylar Asay-Davis - - with open(fileName) as filePointer: - jsonFile = json.load(filePointer) - - for feature in jsonFile['features']: - if feature['properties']['name'] != transectName: - continue - assert(feature['geometry']['type'] == 'LineString') - - coordinates = feature['geometry']['coordinates'] - lon, lat = zip(*coordinates) - break - - dsObs = xarray.Dataset() - dsObs['lon'] = (('nPoints',), numpy.array(lon)) - dsObs.lon.attrs['units'] = 'degrees' - dsObs['lat'] = (('nPoints',), numpy.array(lat)) - dsObs.lat.attrs['units'] = 'degrees' - - return dsObs From 10ce1b40b0edabedc2418701036eb5f7099f3ff7 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 17 Feb 2025 07:28:53 -0600 Subject: [PATCH 031/116] Update the user's guide --- docs/users_guide/analysis_tasks.rst | 2 +- .../tasks/geojsonNetcdfTransects.rst | 448 ++++++++++++++++++ docs/users_guide/tasks/geojsonTransects.rst | 261 ---------- 3 files changed, 449 insertions(+), 262 deletions(-) create mode 100644 docs/users_guide/tasks/geojsonNetcdfTransects.rst delete mode 100644 docs/users_guide/tasks/geojsonTransects.rst diff --git a/docs/users_guide/analysis_tasks.rst b/docs/users_guide/analysis_tasks.rst index d2973b43d..436b5a5f4 100644 --- a/docs/users_guide/analysis_tasks.rst +++ b/docs/users_guide/analysis_tasks.rst @@ -27,7 +27,7 @@ Analysis Tasks tasks/antshipTransects tasks/conservation - tasks/geojsonTransects + tasks/geojsonNetcdfTransects tasks/hovmollerOceanRegions tasks/indexNino34 tasks/meridionalHeatTransport diff --git a/docs/users_guide/tasks/geojsonNetcdfTransects.rst b/docs/users_guide/tasks/geojsonNetcdfTransects.rst new file mode 100644 index 000000000..04604aa8e --- /dev/null +++ b/docs/users_guide/tasks/geojsonNetcdfTransects.rst @@ -0,0 +1,448 @@ +.. _task_geojsonNetcdfTransects: + +geojsonNetcdfTransects +====================== + +An analysis task for interpolating MPAS fields to transects specified by files +in geojson or NetDF format. + +Component and Tags:: + + component: ocean + tags: climatology, transect, geojson, netcdf + +Configuration Options +--------------------- + +The following configuration options are available for this task:: + + [geojsonNetcdfTransects] + ## options related to plotting model transects at points determined by a + ## user-specified geojson or NetCDF file. + ## + ## To generate your own geojson file, go to: + ## http://geojson.io/ + ## and draw one or more polylines, then add a name to each: + ## + ## "properties": { + ## "name": "My Favorite Name" + ## }, + ## and save the file as GeoJSON (say transects.geojson). Finally, set the + ## option: + ## geojsonFiles = ['transects.geojson'] + ## (giving an absolute path if necessary) in your custom config file. + ## + ## If you provide a NetCDF file instead, it simply needs to have 'lat` and + ## `lon` variables. The `lat` and `lon` variables should be 1D arrays + ## with the same dimension name (e.g. 'nPoints'). The name of the file + ## (without the base path or extension) will serve as the transect name with + ## underscores converted to spaces. + + # a list of geojson and/or NetCDF files. The geojson files must contain + # lat/lon points in LineStrings to be plotted. The NetCDF files need 'lat' + # and 'lon' variables with the same dimesion name. If relative paths are + # given, they are relative to the current working directory. The files must + # be listed in quotes, e.g.: + # geojsonOrNetcdfFiles = ['file1.geojson', '/path/to/file2.geojson', 'file3.nc'] + geojsonOrNetcdfFiles = [] + + # a list of dictionaries for each field to plot. The dictionary includes + # prefix (used for file names, task names and sections) as well as the mpas + # name of the field, units for colorbars and a the name as it should appear + # in figure titles and captions. + fields = + [{'prefix': 'temperature', + 'mpas': 'timeMonthly_avg_activeTracers_temperature', + 'units': r'$$\degree$$C', + 'titleName': 'Potential Temperature'}, + {'prefix': 'salinity', + 'mpas': 'timeMonthly_avg_activeTracers_salinity', + 'units': r'PSU', + 'titleName': 'Salinity'}, + {'prefix': 'potentialDensity', + 'mpas': 'timeMonthly_avg_potentialDensity', + 'units': r'kg m$$^{-3}$$', + 'titleName': 'Potential Density'}, + {'prefix': 'zonalVelocity', + 'mpas': 'timeMonthly_avg_velocityZonal', + 'units': r'm s$$^{-1}$$', + 'titleName': 'Zonal Velocity'}, + {'prefix': 'meridionalVelocity', + 'mpas': 'timeMonthly_avg_velocityMeridional', + 'units': r'm s$$^{-1}$$', + 'titleName': 'Meridional Velocity'}, + {'prefix': 'vertVelocity', + 'mpas': 'timeMonthly_avg_vertVelocityTop', + 'units': r'm s$$^{-1}$$', + 'titleName': 'Vertical Velocity'}, + {'prefix': 'vertDiff', + 'mpas': 'timeMonthly_avg_vertDiffTopOfCell', + 'units': r'm s$$^{-1}$$', + 'titleName': 'Vertical Diffusivity'}, + {'prefix': 'vertVisc', + 'mpas': 'timeMonthly_avg_vertViscTopOfCell', + 'units': r'm s$$^{-1}$$', + 'titleName': 'Vertical Viscosity'}, + ] + + # Times for comparison times (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, + # Nov, Dec, JFM, AMJ, JAS, OND, ANN) + seasons = ['ANN'] + + # The approximate horizontal resolution (in km) of each transect. Latitude/ + # longitude between observation points will be subsampled at this interval. + # Use 'obs' to indicate no subsampling. Use 'mpas' to indicate plotting of + # model data on the native grid. + #horizontalResolution = mpas + #horizontalResolution = obs + horizontalResolution = 5 + + # The name of the vertical comparison grid. Valid values are 'mpas' for the + # MPAS vertical grid or any other name if the vertical grid is defined by + # 'verticalComparisonGrid' + #verticalComparisonGridName = mpas + verticalComparisonGridName = uniform_0_to_4000m_at_10m + + # The vertical comparison grid if 'verticalComparisonGridName' is not 'mpas'. + # This should be numpy array of (typically negative) elevations (in m). + # The first and last entries are used as axis bounds for 'mpas' vertical + # comparison grids + verticalComparisonGrid = numpy.linspace(0, -4000, 401) + + # A range for the y axis (if any) + verticalBounds = [] + + # The minimum weight of a destination cell after remapping. Any cell with + # weights lower than this threshold will therefore be masked out. + renormalizationThreshold = 0.01 + + + [geojsonNetcdfTemperatureTransects] + ## options related to plotting geojson transects of potential temperature + + # colormap for model/observations + colormapNameResult = RdYlBu_r + # whether the colormap is indexed or continuous + colormapTypeResult = continuous + # the type of norm used in the colormap + normTypeResult = linear + # A dictionary with keywords for the norm + normArgsResult = {'vmin': -2., 'vmax': 30.} + # place the ticks automatically by default + # colorbarTicksResult = numpy.linspace(-2., 2., 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsResult = [] + + # colormap for differences + colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = continuous + # the type of norm used in the colormap + normTypeDifference = linear + # A dictionary with keywords for the norm + normArgsDifference = {'vmin': -2., 'vmax': 2.} + # place the ticks automatically by default + # colorbarTicksDifference = numpy.linspace(-2., 2., 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsDifference = [] + + + [geojsonNetcdfSalinityTransects] + ## options related to plotting geojson transects of salinity + + # colormap for model/observations + colormapNameResult = haline + # whether the colormap is indexed or continuous + colormapTypeResult = continuous + # the type of norm used in the colormap + normTypeResult = linear + # A dictionary with keywords for the norm + normArgsResult = {'vmin': 30, 'vmax': 39.0} + # place the ticks automatically by default + # colorbarTicksResult = numpy.linspace(34.2, 35.2, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsResult = [] + + # colormap for differences + colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = continuous + # the type of norm used in the colormap + normTypeDifference = linear + # A dictionary with keywords for the norm + normArgsDifference = {'vmin': -0.5, 'vmax': 0.5} + # place the ticks automatically by default + # colorbarTicksDifference = numpy.linspace(-0.5, 0.5, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsDifference = [] + + + [geojsonNetcdfPotentialDensityTransects] + ## options related to plotting geojson transects of potential density + + # colormap for model/observations + colormapNameResult = Spectral_r + # whether the colormap is indexed or continuous + colormapTypeResult = continuous + # the type of norm used in the colormap + normTypeResult = linear + # A dictionary with keywords for the norm + normArgsResult = {'vmin': 1026.5, 'vmax': 1028.} + # place the ticks automatically by default + # colorbarTicksResult = numpy.linspace(1026., 1028., 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsResult = [] + + # colormap for differences + colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = continuous + # the type of norm used in the colormap + normTypeDifference = linear + # A dictionary with keywords for the norm + normArgsDifference = {'vmin': -0.3, 'vmax': 0.3} + # place the ticks automatically by default + # colorbarTicksDifference = numpy.linspace(-0.3, 0.3, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsDifference = [] + + + [geojsonNetcdfZonalVelocityTransects] + ## options related to plotting geojson transects of zonal velocity + + # colormap for model/observations + colormapNameResult = delta + # whether the colormap is indexed or continuous + colormapTypeResult = continuous + # the type of norm used in the colormap + normTypeResult = linear + # A dictionary with keywords for the norm + normArgsResult = {'vmin': -0.05, 'vmax': 0.05} + # determine the ticks automatically by default, uncomment to specify + # colorbarTicksResult = numpy.linspace(-0.2, 0.2, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsResult = 'none' + + # colormap for differences + colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = continuous + # the type of norm used in the colormap + normTypeDifference = linear + # A dictionary with keywords for the norm + normArgsDifference = {'vmin': -0.05, 'vmax': 0.05} + # determine the ticks automatically by default, uncomment to specify + # colorbarTicksDifference = numpy.linspace(-0.2, 0.2, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsDifference = 'none' + + + [geojsonNetcdfMeridionalVelocityTransects] + ## options related to plotting geojson transects of meridional velocity + + # colormap for model/observations + colormapNameResult = delta + # whether the colormap is indexed or continuous + colormapTypeResult = continuous + # the type of norm used in the colormap + normTypeResult = linear + # A dictionary with keywords for the norm + normArgsResult = {'vmin': -0.05, 'vmax': 0.05} + # determine the ticks automatically by default, uncomment to specify + # colorbarTicksResult = numpy.linspace(-0.2, 0.2, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsResult = 'none' + + # colormap for differences + colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = continuous + # the type of norm used in the colormap + normTypeDifference = linear + # A dictionary with keywords for the norm + normArgsDifference = {'vmin': -0.05, 'vmax': 0.05} + # determine the ticks automatically by default, uncomment to specify + # colorbarTicksDifference = numpy.linspace(-0.2, 0.2, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsDifference = 'none' + + [geojsonNetcdfVertVelocityTransects] + ## options related to plotting geojson transects of meridional velocity + + # colormap for model/observations + colormapNameResult = delta + # whether the colormap is indexed or continuous + colormapTypeResult = continuous + # the type of norm used in the colormap + normTypeResult = linear + # A dictionary with keywords for the norm + normArgsResult = {'vmin': -1e-5, 'vmax': 1e-5} + # determine the ticks automatically by default, uncomment to specify + # colorbarTicksResult = numpy.linspace(-0.2, 0.2, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsResult = 'none' + + # colormap for differences + colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = continuous + # the type of norm used in the colormap + normTypeDifference = linear + # A dictionary with keywords for the norm + normArgsDifference = {'vmin': -1e-5, 'vmax': 1e-5} + # determine the ticks automatically by default, uncomment to specify + # colorbarTicksDifference = numpy.linspace(-0.2, 0.2, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsDifference = 'none' + + [geojsonNetcdfVertDiffTransects] + ## options related to plotting geojson transects of meridional velocity + + # colormap for model/observations + colormapNameResult = diff + # whether the colormap is indexed or continuous + colormapTypeResult = continuous + # the type of norm used in the colormap + normTypeResult = linear + # A dictionary with keywords for the norm + normArgsResult = {'vmin': -0.5, 'vmax': 0.5} + # determine the ticks automatically by default, uncomment to specify + # colorbarTicksResult = numpy.linspace(-0.2, 0.2, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsResult = 'none' + + # colormap for differences + colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = continuous + # the type of norm used in the colormap + normTypeDifference = linear + # A dictionary with keywords for the norm + normArgsDifference = {'vmin': -0.5, 'vmax': 0.5} + # determine the ticks automatically by default, uncomment to specify + # colorbarTicksDifference = numpy.linspace(-0.2, 0.2, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsDifference = 'none' + + [geojsonNetcdfVertViscTransects] + ## options related to plotting geojson transects of meridional velocity + + # colormap for model/observations + colormapNameResult = diff + # whether the colormap is indexed or continuous + colormapTypeResult = continuous + # the type of norm used in the colormap + normTypeResult = linear + # A dictionary with keywords for the norm + normArgsResult = {'vmin': -1., 'vmax': 1.} + # determine the ticks automatically by default, uncomment to specify + # colorbarTicksResult = numpy.linspace(-0.2, 0.2, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsResult = 'none' + + # colormap for differences + colormapNameDifference = balance + # whether the colormap is indexed or continuous + colormapTypeDifference = continuous + # the type of norm used in the colormap + normTypeDifference = linear + # A dictionary with keywords for the norm + normArgsDifference = {'vmin': -1., 'vmax': 1.} + # determine the ticks automatically by default, uncomment to specify + # colorbarTicksDifference = numpy.linspace(-0.2, 0.2, 9) + # contour line levels (use [] for automatic contour selection, 'none' for no + # contour lines) + contourLevelsDifference = 'none' + + +Geojson Files +------------- + +This task takes a list of geojson or NetCDF file names (supplied as a python +list of ``str``):: + + geojsonOrNetcdfFiles = ['file1.geojson', '/path/to/file2.geojson'] + +Geojson transects are specified by ``LineString`` objects in the files. Some +examples are provided in the `MPAS geometric features repository`_. You can +also generate your own very easily at To generate your own geojson file, go to +`geojson.io`_ and draw one or more polylines, then add a name to each:: + + ... + "properties": { + "name": "My Favorite Name" + }, + ... + +and save the file as GeoJSON (say transects.geojson). Finally, set the +option:: + + geojsonFiles = ['transects.geojson'] + +(giving an absolute path if necessary) in your custom config file. + + +NetCDF Files +------------ + +As an alternative to (or in addition to) geojson files, you may supply files in +NetCDF format. As before, you provide a list of file names as a python +list of ``str``:: + + geojsonOrNetcdfFiles = ['file1.nc', '/path/to/file2.nc'] + +In this case, the stem of the filename (``file1`` and ``file2`` in the example) +also serves as the name of the transect. The NetCDF files must contain +``lat`` and ``lon`` variables. These variables should be 1D arrays with the +same dimension name (e.g. ``nPoints``). + +Fields +------ + +Since there are no observations associated with geojson transects, you are +free to choose which MPAS fields you would like to plot. These fields are +provided as a python dictionary. The keys are names for the fields (anything +you would like use as a prefix on files and subtask names, best if it does +not contain spaces). The values are python dictionaries. The values +associate with the ``mpas`` key are the names of the 3D fields where transects +are desired. The ``units`` entry indicates the units to display on the +colorbar. The ``titleName`` entry specifies the name of the field to include +in plot titles and captions. + +Each field must have a corresponding section in the config file defining its +color maps. For example, ``temperature`` has an associated +``[geojsonNetcdfTemperatureTransect]`` section. + +Other Options +------------- + +For details on the remaining configuration options, see: + * :ref:`config_transects` + * :ref:`config_remapping` + * :ref:`config_colormaps` + * :ref:`config_seasons` + +Example Result +-------------- + +.. image:: examples/geojson_transect.png + :width: 500 px + :align: center + +.. _`MPAS geometric features repository`: https://github.com/MPAS-Dev/geometric_features +.. _`geojson.io`: http://geojson.io/ diff --git a/docs/users_guide/tasks/geojsonTransects.rst b/docs/users_guide/tasks/geojsonTransects.rst deleted file mode 100644 index da611e93c..000000000 --- a/docs/users_guide/tasks/geojsonTransects.rst +++ /dev/null @@ -1,261 +0,0 @@ -.. _task_geojsonTransects: - -geojsonTransects -================ - -An analysis task for interpolating MPAS fields to transects specified by files -in ``geojson`` format.. - -Component and Tags:: - - component: ocean - tags: climatology, transect, geojson - -Configuration Options ---------------------- - -The following configuration options are available for this task:: - - [geojsonTransects] - ## options related to plotting model transects at points determined by a - ## geojson file. To generate your own geojson file, go to: - ## http://geojson.io/ - ## and draw one or more polylines, then add a name to each: - ## - ## "properties": { - ## "name": "My Favorite Name" - ## }, - ## and save the file as GeoJSON (say transects.geojson). Finally, set the - ## option: - ## geojsonFiles = ['transects.geojson'] - ## (giving an absolute path if necessary) in your custom config file. - - # a list of geojson files containing lat/lon points in LineStrings to be - # plotted. If relative paths are given, they are relative to the current - # working directory. The files must be listed in quotes, e.g.: - # geojsonFiles = ['file1.geojson', '/path/to/file2.geojson'] - geojsonFiles = [] - - # a list of dictionaries for each field to plot. The dictionary includes - # prefix (used for file names, task names and sections) as well as the mpas - # name of the field, units for colorbars and a the name as it should appear - # in figure titles and captions. - fields = - [{'prefix': 'temperature', - 'mpas': 'timeMonthly_avg_activeTracers_temperature', - 'units': r'$\degree$C', - 'titleName': 'Potential Temperature'}, - {'prefix': 'salinity', - 'mpas': 'timeMonthly_avg_activeTracers_salinity', - 'units': r'PSU', - 'titleName': 'Salinity'}, - {'prefix': 'potentialDensity', - 'mpas': 'timeMonthly_avg_potentialDensity', - 'units': r'kg m$^{-3}$', - 'titleName': 'Potential Density'}, - {'prefix': 'zonalVelocity', - 'mpas': 'timeMonthly_avg_velocityZonal', - 'units': r'm s$^{-1}$', - 'titleName': 'Zonal Velocity'}, - {'prefix': 'meridionalVelocity', - 'mpas': 'timeMonthly_avg_velocityMeridional', - 'units': r'm s$^{-1}$', - 'titleName': 'Meridional Velocity'}] - - # Times for comparison times (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, - # Nov, Dec, JFM, AMJ, JAS, OND, ANN) - seasons = ['ANN'] - - # The approximate horizontal resolution (in km) of each transect. Latitude/ - # longitude between observation points will be subsampled at this interval. - # Use 'obs' to indicate no subsampling. - horizontalResolution = 5 - - # The name of the vertical comparison grid. Valid values are 'mpas' for the - # MPAS vertical grid or any other name if the vertical grid is defined by - # 'verticalComparisonGrid' - #verticalComparisonGridName = mpas - verticalComparisonGridName = uniform_0_to_4000m_at_10m - - # The vertical comparison grid if 'verticalComparisonGridName' is not 'mpas'. - # This should be numpy array of (typically negative) elevations (in m). - verticalComparisonGrid = numpy.linspace(0, -4000, 401) - - # The minimum weight of a destination cell after remapping. Any cell with - # weights lower than this threshold will therefore be masked out. - renormalizationThreshold = 0.01 - - - [geojsonTemperatureTransects] - ## options related to plotting geojson transects of potential temperature - - # colormap for model/observations - colormapNameResult = RdYlBu_r - # the type of norm used in the colormap - normTypeResult = linear - # A dictionary with keywords for the SemiLogNorm - normArgsResult = {'vmin': -2., 'vmax': 30.} - # place the ticks automatically by default - # colorbarTicksResult = numpy.linspace(-2., 2., 9) - - # colormap for differences - colormapNameDifference = balance - # the type of norm used in the colormap - normTypeDifference = linear - # A dictionary with keywords for the SemiLogNorm - normArgsDifference = {'vmin': -2., 'vmax': 2.} - # place the ticks automatically by default - # colorbarTicksDifference = numpy.linspace(-2., 2., 9) - - - [geojsonSalinityTransects] - ## options related to plotting geojson transects of salinity - - # colormap for model/observations - colormapNameResult = haline - # the type of norm used in the colormap - normTypeResult = linear - # A dictionary with keywords for the SemiLogNorm - normArgsResult = {'vmin': 30, 'vmax': 39.0} - # place the ticks automatically by default - # colorbarTicksResult = numpy.linspace(34.2, 35.2, 9) - - # colormap for differences - colormapNameDifference = balance - # the type of norm used in the colormap - normTypeDifference = linear - # A dictionary with keywords for the SemiLogNorm - normArgsDifference = {'vmin': -0.5, 'vmax': 0.5} - # place the ticks automatically by default - # colorbarTicksDifference = numpy.linspace(-0.5, 0.5, 9) - - - [geojsonPotentialDensityTransects] - ## options related to plotting geojson transects of potential density - - # colormap for model/observations - colormapNameResult = Spectral_r - # the type of norm used in the colormap - normTypeResult = linear - # A dictionary with keywords for the norm - normArgsResult = {'vmin': 1026.5, 'vmax': 1028.} - # place the ticks automatically by default - # colorbarTicksResult = numpy.linspace(1026., 1028., 9) - - # colormap for differences - colormapNameDifference = balance - # the type of norm used in the colormap - normTypeDifference = linear - # A dictionary with keywords for the norm - normArgsDifference = {'vmin': -0.3, 'vmax': 0.3} - # place the ticks automatically by default - # colorbarTicksDifference = numpy.linspace(-0.3, 0.3, 9) - - - [geojsonZonalVelocityTransects] - ## options related to plotting geojson transects of zonal velocity - - # colormap for model/observations - colormapNameResult = delta - # color indices into colormapName for filled contours - # the type of norm used in the colormap - normTypeResult = linear - # A dictionary with keywords for the norm - normArgsResult = {'vmin': -0.2, 'vmax': 0.2} - # determine the ticks automatically by default, uncomment to specify - # colorbarTicksResult = numpy.linspace(-0.2, 0.2, 9) - - # colormap for differences - colormapNameDifference = balance - # the type of norm used in the colormap - normTypeDifference = linear - # A dictionary with keywords for the norm - normArgsDifference = {'vmin': -0.2, 'vmax': 0.2} - # determine the ticks automatically by default, uncomment to specify - # colorbarTicksDifference = numpy.linspace(-0.2, 0.2, 9) - - - [geojsonMeridionalVelocityTransects] - ## options related to plotting geojson transects of meridional velocity - - # colormap for model/observations - colormapNameResult = delta - # color indices into colormapName for filled contours - # the type of norm used in the colormap - normTypeResult = linear - # A dictionary with keywords for the norm - normArgsResult = {'vmin': -0.2, 'vmax': 0.2} - # determine the ticks automatically by default, uncomment to specify - # colorbarTicksResult = numpy.linspace(-0.2, 0.2, 9) - - # colormap for differences - colormapNameDifference = balance - # the type of norm used in the colormap - normTypeDifference = linear - # A dictionary with keywords for the norm - normArgsDifference = {'vmin': -0.2, 'vmax': 0.2} - # determine the ticks automatically by default, uncomment to specify - # colorbarTicksDifference = numpy.linspace(-0.2, 0.2, 9) - -Geojson Files -------------- - -This task takes a list of geojson file names (supplied as a python list of -``str``:: - - geojsonFiles = ['file1.geojson', '/path/to/file2.geojson'] - -Transects are specified by ``LineString`` objects in the files. Some examples -are provided in the `MPAS geometric features repository`_. You can also -generate your own very easily at To generate your own geojson file, go to -`geojson.io`_ and draw one or more polylines, then add a name to each:: - - ... - "properties": { - "name": "My Favorite Name" - }, - ... - -and save the file as GeoJSON (say transects.geojson). Finally, set the -option:: - - geojsonFiles = ['transects.geojson'] - -(giving an absolute path if necessary) in your custom config file. - - -Fields ------- - -Since there are no observations associated with geojson transects, you are -free to choose which MPAS fields you would like to plot. These fields are -provided as a python dictionary. The keys are names for the fields (anything -you would like use as a prefix on files and subtask names, best if it does -not contain spaces). The values are python dictionaries. The values -associate with the ``mpas`` key are the names of the 3D fields where transects -are desired. The ``units`` entry indicates the units to display on the -colorbar. The ``titleName`` entry specifies the name of the field to include -in plot titles and captions. - -Each field must have a corresponding section in the config file defining its -color maps. For example, ``temperature`` has an associated -``[geojsonTemperatureTransect]`` section. - -Other Options -------------- - -For details on the remaining configuration options, see: - * :ref:`config_transects` - * :ref:`config_remapping` - * :ref:`config_colormaps` - * :ref:`config_seasons` - -Example Result --------------- - -.. image:: examples/geojson_transect.png - :width: 500 px - :align: center - -.. _`MPAS geometric features repository`: https://github.com/MPAS-Dev/geometric_features -.. _`geojson.io`: http://geojson.io/ From f7a7e0bfa3cf011cb6aeb87bcbfb2bf5af678c84 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sun, 20 Apr 2025 12:51:37 -0600 Subject: [PATCH 032/116] Add GeojsonNetcdfTransects to API --- docs/developers_guide/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/developers_guide/api.rst b/docs/developers_guide/api.rst index 659916754..3a93b1a59 100644 --- a/docs/developers_guide/api.rst +++ b/docs/developers_guide/api.rst @@ -78,6 +78,7 @@ Ocean tasks ClimatologyMapArgoSalinity ClimatologyMapWaves ClimatologyMapCustom + GeojsonNetcdfTransects IndexNino34 MeridionalHeatTransport OceanHistogram From e9ca77e94c3ed87540ad4d55c50af4dd3bb1a843 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 22 Apr 2025 11:33:06 -0500 Subject: [PATCH 033/116] Separate lists of availableVariables and variables This is more convenient and intuitive for plotting a subset of variables. --- mpas_analysis/default.cfg | 15 ++++++++++----- mpas_analysis/ocean/geojson_netcdf_transects.py | 7 ++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/mpas_analysis/default.cfg b/mpas_analysis/default.cfg index cc7175a47..0c29e4eba 100755 --- a/mpas_analysis/default.cfg +++ b/mpas_analysis/default.cfg @@ -3646,11 +3646,11 @@ contourLevelsDifference = np.arange(-0.9, 1.0, 0.4) # geojsonOrNetcdfFiles = ['file1.geojson', '/path/to/file2.geojson', 'file3.nc'] geojsonOrNetcdfFiles = [] -# a list of dictionaries for each field to plot. The dictionary includes -# prefix (used for file names, task names and sections) as well as the mpas -# name of the field, units for colorbars and a the name as it should appear -# in figure titles and captions. -fields = +# a list of dictionaries for each field available to plot. The dictionary +# includes prefix (used for file names, task names and sections) as well as the +# mpas name of the field, units for colorbars and a the name as it should +# appear in figure titles and captions. +availableVariables = [{'prefix': 'temperature', 'mpas': 'timeMonthly_avg_activeTracers_temperature', 'units': r'$$\degree$$C', @@ -3685,6 +3685,11 @@ fields = 'titleName': 'Vertical Viscosity'}, ] +# a list of the prefixes from availableVariables that should be plotted +variables = ['temperature', 'salinity', 'potentialDensity', + 'zonalVelocity', 'meridionalVelocity', 'vertVelocity', + 'vertDiff', 'vertVisc'] + # Times for comparison times (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, # Nov, Dec, JFM, AMJ, JAS, OND, ANN) seasons = ['ANN'] diff --git a/mpas_analysis/ocean/geojson_netcdf_transects.py b/mpas_analysis/ocean/geojson_netcdf_transects.py index 18c4fe4f7..203bbb222 100644 --- a/mpas_analysis/ocean/geojson_netcdf_transects.py +++ b/mpas_analysis/ocean/geojson_netcdf_transects.py @@ -80,7 +80,12 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): verticalComparisonGrid = config.getexpression( sectionName, 'verticalComparisonGrid', use_numpyfunc=True) - fields = config.getexpression(sectionName, 'fields') + availableVariables = config.getexpression( + sectionName, 'availableVariables') + + prefixes = config.getexpression(sectionName, 'variables') + fields = [field for field in availableVariables + if field['prefix'] in prefixes] geojsonFileNames = {} netcdfFileNames = {} From 7ac3ba5e6fd9bc4427eee82c11715afca3de5c74 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 26 Apr 2025 10:56:24 -0500 Subject: [PATCH 034/116] Make sure transect obs dims are transposed like model dims --- mpas_analysis/ocean/plot_transect_subtask.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mpas_analysis/ocean/plot_transect_subtask.py b/mpas_analysis/ocean/plot_transect_subtask.py index a144872d2..22b187f60 100644 --- a/mpas_analysis/ocean/plot_transect_subtask.py +++ b/mpas_analysis/ocean/plot_transect_subtask.py @@ -434,6 +434,8 @@ def _plot_transect(self, remappedModelClimatology, remappedRefClimatology): bias = None else: refOutput = remappedRefClimatology[self.refFieldName] + # make sure the dimension order is the same + refOutput = refOutput.transpose(*modelOutput.dims) bias = modelOutput - refOutput filePrefix = self.filePrefix From 1677ef33a1de6ec22647546075554d84df8f82c9 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 11 Jun 2025 09:06:44 -0500 Subject: [PATCH 035/116] Reorder calls for custom climatology maps This is needed so we have the full 3D variables when computing custom derived quantities like thermal forcing (which needs the full 3D density and layer thickness). --- mpas_analysis/ocean/climatology_map_custom.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/mpas_analysis/ocean/climatology_map_custom.py b/mpas_analysis/ocean/climatology_map_custom.py index d714ccbb6..fb60eb3f9 100644 --- a/mpas_analysis/ocean/climatology_map_custom.py +++ b/mpas_analysis/ocean/climatology_map_custom.py @@ -269,14 +269,19 @@ def customize_masked_climatology(self, climatology, season): the modified climatology data set """ - # first, call the base class's version of this function so we extract - # the desired slices. - climatology = super().customize_masked_climatology(climatology, - season) + # first, compute the derived variables, which may rely on having the + # full 3D variables available + derivedVars = [] self._add_vel_mag(climatology, derivedVars) self._add_thermal_forcing(climatology, derivedVars) + # then, call the superclass's version of this function so we extract + # the desired slices (but before renaming because it expects the + # original MPAS variable names) + climatology = super().customize_masked_climatology(climatology, + season) + # finally, rename the variables and add metadata for varName, variable in self.variables.items(): if varName not in derivedVars: # rename variables from MPAS names to shorter names @@ -334,6 +339,8 @@ def _add_thermal_forcing(self, climatology, derivedVars): thick = climatology.timeMonthly_avg_layerThickness dp = cime_constants['SHR_CONST_G']*dens*thick + # NOTE: this is missing the surface pressure, which is not available + # in the timeSeriesStatsMonthly output press = dp.cumsum(dim='nVertLevels') - 0.5*dp tempFreeze = c0 + cs*salin + cp*press + cps*press*salin From 1a819ed5752ae9484e5842b83f0ad783bde9abc5 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 11 Jun 2025 09:16:09 -0500 Subject: [PATCH 036/116] Add land-ice pressure to the pressure used for thermal forcing --- mpas_analysis/ocean/climatology_map_custom.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mpas_analysis/ocean/climatology_map_custom.py b/mpas_analysis/ocean/climatology_map_custom.py index fb60eb3f9..1761b6d15 100644 --- a/mpas_analysis/ocean/climatology_map_custom.py +++ b/mpas_analysis/ocean/climatology_map_custom.py @@ -10,6 +10,7 @@ # https://raw.githubusercontent.com/MPAS-Dev/MPAS-Analysis/main/LICENSE import numpy as np +import xarray as xr from mpas_tools.cime.constants import constants as cime_constants from mpas_analysis.ocean.remap_depth_slices_subtask import ( @@ -339,10 +340,14 @@ def _add_thermal_forcing(self, climatology, derivedVars): thick = climatology.timeMonthly_avg_layerThickness dp = cime_constants['SHR_CONST_G']*dens*thick - # NOTE: this is missing the surface pressure, which is not available - # in the timeSeriesStatsMonthly output press = dp.cumsum(dim='nVertLevels') - 0.5*dp + # add land ice pressure if available + ds_restart = xr.open_dataset(self.restartFileName) + ds_restart = ds_restart.isel(Time=0) + if 'landIcePressure' in ds_restart: + press += ds_restart.landIcePressure + tempFreeze = c0 + cs*salin + cp*press + cps*press*salin climatology[varName] = temp - tempFreeze From 5abecfbf2493bdce19ecced3a06824d0d42fe6b9 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 11 Jun 2025 09:19:40 -0500 Subject: [PATCH 037/116] Fix comment in default config options --- mpas_analysis/default.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mpas_analysis/default.cfg b/mpas_analysis/default.cfg index 0c29e4eba..861134213 100755 --- a/mpas_analysis/default.cfg +++ b/mpas_analysis/default.cfg @@ -2225,8 +2225,8 @@ availableVariables = { 'has_depth': False}, } -# a list of fields top plot for each transect. All supported fields are listed -# below +# a list of fields top plot for each depth slice. All supported fields are +# listed above variables = [] From ace7d9fab3c101d04bc4cb75b23de4af4cc5e076 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 11 Jun 2025 09:21:50 -0500 Subject: [PATCH 038/116] Add custom variables to "main" test in suite --- suite/main.cfg | 19 +++++++++++++++++++ suite/setup.py | 4 ++++ 2 files changed, 23 insertions(+) create mode 100644 suite/main.cfg diff --git a/suite/main.cfg b/suite/main.cfg new file mode 100644 index 000000000..b8909f14e --- /dev/null +++ b/suite/main.cfg @@ -0,0 +1,19 @@ +[climatologyMapCustom] +## options related to plotting climatology maps of any field at various depths +## (if they include a depth dimension) without observatons for comparison + +# a list of fields top plot for each depth slice. All supported fields are +# listed above +variables = [ + 'temperature', + 'salinity', + 'potentialDensity', + 'thermalForcing', + 'zonalVelocity', + 'meridionalVelocity', + 'velocityMagnitude', + 'vertVelocity', + 'vertDiff', + 'vertVisc', + 'mixedLayerDepth' + ] diff --git a/suite/setup.py b/suite/setup.py index dbab86a75..df41ce0e2 100755 --- a/suite/setup.py +++ b/suite/setup.py @@ -146,6 +146,10 @@ def main(): [config_from_job, os.path.join('..', '..', 'suite', f'{args.run}.cfg')]) + if args.run.startswith('main_py'): + config_from_job = ' '.join( + [config_from_job, os.path.join('..', '..', 'suite', 'main.cfg')]) + if args.run not in ['main', 'ctrl']: try: os.makedirs(os.path.join(suite_path, args.run)) From f68d40818b413de5734d0d9edb5dd60c9e872c06 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 11 Jun 2025 10:29:02 -0500 Subject: [PATCH 039/116] Fix incorrect variable name in custom climatology --- mpas_analysis/default.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpas_analysis/default.cfg b/mpas_analysis/default.cfg index 861134213..d3acd6cc2 100755 --- a/mpas_analysis/default.cfg +++ b/mpas_analysis/default.cfg @@ -2192,7 +2192,7 @@ availableVariables = { 'mpas': ['timeMonthly_avg_activeTracers_temperature', 'timeMonthly_avg_activeTracers_salinity', 'timeMonthly_avg_density', - 'timeMonthly_avg_activeTracers_layerThickness']}, + 'timeMonthly_avg_layerThickness']}, 'zonalVelocity': {'title': 'Zonal Velocity', 'units': r'm s$$^{-1}$$', From 69e9d9e0d6bc051be6b2c3213fab26dc3a916da8 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 11 Jun 2025 11:27:01 -0500 Subject: [PATCH 040/116] Fix typo in velocity magnitude variable name --- mpas_analysis/ocean/climatology_map_custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpas_analysis/ocean/climatology_map_custom.py b/mpas_analysis/ocean/climatology_map_custom.py index 1761b6d15..1a0e1b847 100644 --- a/mpas_analysis/ocean/climatology_map_custom.py +++ b/mpas_analysis/ocean/climatology_map_custom.py @@ -303,7 +303,7 @@ def _add_vel_mag(self, climatology, derivedVars): """ variables = self.variables - varName = 'verlocityMagnitude' + varName = 'velocityMagnitude' if varName not in variables: return From dcb73f3b1eb7088652f23338809ca1073e627ce3 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 11 Jun 2025 13:08:34 -0500 Subject: [PATCH 041/116] Slice all variables with a vertical dimension Before, we were slicing only those that are in the original variable list but that leads to problems if new custom variables have been added. --- mpas_analysis/ocean/remap_depth_slices_subtask.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mpas_analysis/ocean/remap_depth_slices_subtask.py b/mpas_analysis/ocean/remap_depth_slices_subtask.py index f145fcdb3..4d9cbd03c 100644 --- a/mpas_analysis/ocean/remap_depth_slices_subtask.py +++ b/mpas_analysis/ocean/remap_depth_slices_subtask.py @@ -248,7 +248,9 @@ def customize_masked_climatology(self, climatology, season): interfaceIndices = self.dsSlice.interfaceIndices interfaceIndexMask = self.dsSlice.interfaceIndexMask - for variableName in self.variableList: + # iterate over all variables since some new ones may have been + # added by a subclass + for variableName in climatology.data_vars: if 'nVertLevels' in climatology[variableName].dims: # mask only the values with the right vertical index da = climatology[variableName].where( From 2f6188e8043569081d8f1cb477e37eefc80ded14 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 11 Jun 2025 13:09:33 -0500 Subject: [PATCH 042/116] A little cleanup --- mpas_analysis/ocean/climatology_map_custom.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/mpas_analysis/ocean/climatology_map_custom.py b/mpas_analysis/ocean/climatology_map_custom.py index 1a0e1b847..b3c0e3447 100644 --- a/mpas_analysis/ocean/climatology_map_custom.py +++ b/mpas_analysis/ocean/climatology_map_custom.py @@ -301,10 +301,8 @@ def _add_vel_mag(self, climatology, derivedVars): """ Add the velocity magnitude to the climatology if requested """ - variables = self.variables - varName = 'velocityMagnitude' - if varName not in variables: + if varName not in self.variables: return derivedVars.append(varName) @@ -317,10 +315,8 @@ def _add_thermal_forcing(self, climatology, derivedVars): """ Add thermal forcing to the climatology if requested """ - variables = self.variables - varName = 'thermalForcing' - if varName not in variables: + if varName not in self.variables: return derivedVars.append(varName) From 96d037ce11d6a5e5bc00443453bdc880326c9c1c Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 11 Jun 2025 14:02:10 -0500 Subject: [PATCH 043/116] Give custom climatology images a unique prefix --- mpas_analysis/ocean/climatology_map_custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpas_analysis/ocean/climatology_map_custom.py b/mpas_analysis/ocean/climatology_map_custom.py index b3c0e3447..7b6aa248c 100644 --- a/mpas_analysis/ocean/climatology_map_custom.py +++ b/mpas_analysis/ocean/climatology_map_custom.py @@ -174,7 +174,7 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): subtaskName=subtaskName) subtask.set_plot_info( - outFileLabel=varName, + outFileLabel=f'cust_{varName}', fieldNameInTitle=title, mpasFieldName=mpasVarName, refFieldName=mpasVarName, From eeb6e95e814de6cc76b2017cf3f99732a6148126 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 11 Jun 2025 14:54:23 -0500 Subject: [PATCH 044/116] Fix velocity magnitude title --- mpas_analysis/default.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpas_analysis/default.cfg b/mpas_analysis/default.cfg index d3acd6cc2..bdb6c6174 100755 --- a/mpas_analysis/default.cfg +++ b/mpas_analysis/default.cfg @@ -2202,7 +2202,7 @@ availableVariables = { 'units': r'm s$$^{-1}$$', 'mpas': ['timeMonthly_avg_velocityMeridional']}, 'velocityMagnitude': - {'title': 'Zonal Velocity', + {'title': 'Velocity Magnitude', 'units': r'm s$$^{-1}$$', 'mpas': ['timeMonthly_avg_velocityZonal', 'timeMonthly_avg_velocityMeridional']}, From c2e5e950e560ea6698c70dedee02d155da3accad Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 11 Jun 2025 14:59:02 -0500 Subject: [PATCH 045/116] Switch `download_data.py` to use `importlib.resources` --- mpas_analysis/download_data.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mpas_analysis/download_data.py b/mpas_analysis/download_data.py index 3c70aeea6..ce850ef9d 100755 --- a/mpas_analysis/download_data.py +++ b/mpas_analysis/download_data.py @@ -19,7 +19,7 @@ import argparse -import pkg_resources +import importlib.resources as resources import os from mpas_analysis.shared.io.download import download_files @@ -49,9 +49,10 @@ def download_analysis_data(): pass urlBase = 'https://web.lcrc.anl.gov/public/e3sm/diagnostics' - analysisFileList = pkg_resources.resource_string( - 'mpas_analysis', - 'obs/{}_input_files'.format(args.dataset)).decode('utf-8') + resource = resources.files('mpas_analysis.obs').joinpath( + f'{args.dataset}_input_files') + with resource.open('r') as f: + analysisFileList = f.read() # remove any empty strings from the list analysisFileList = list(filter(None, analysisFileList.split('\n'))) From 9c2e7b1148c63dbbcfb1773f3efd8203301f8325 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 11 Jun 2025 14:59:36 -0500 Subject: [PATCH 046/116] Switch html generation to use `importlib.resources` --- mpas_analysis/shared/html/pages.py | 97 ++++++++----------- .../shared/html/templates/__init__.py | 0 2 files changed, 38 insertions(+), 59 deletions(-) create mode 100644 mpas_analysis/shared/html/templates/__init__.py diff --git a/mpas_analysis/shared/html/pages.py b/mpas_analysis/shared/html/pages.py index 883940dec..302ce7dd0 100644 --- a/mpas_analysis/shared/html/pages.py +++ b/mpas_analysis/shared/html/pages.py @@ -16,7 +16,7 @@ from os import makedirs from pathlib import Path -import pkg_resources +import importlib.resources as resources from lxml import etree import mpas_analysis.version @@ -66,13 +66,13 @@ def generate_html(config, analyses, controlConfig, customConfigFiles): try: ComponentPage.add_image(fileName, config, components, controlConfig) - except IOError: - print(' missing file {}'.format(fileName)) + except IOError as e: + print(f'Error reading {fileName}: {e}') missingCount += 1 if missingCount > 0: - print('Warning: {} XML files were missing and the analysis website' - ' will be incomplete.'.format(missingCount)) + print(f'Warning: {missingCount} XML files could not be read and the ' + f'analysis website will be incomplete.') # generate the page for each component and add the component to the main # page for componentName, component in components.items(): @@ -155,24 +155,15 @@ def __init__(self, config, controlConfig, customConfigFiles): self.customConfigFiles = customConfigFiles # get template text - fileName = \ - pkg_resources.resource_filename(__name__, - "templates/main_page.html") - - with open(fileName, 'r') as templateFile: - self.pageTemplate = templateFile.read() - - fileName = \ - pkg_resources.resource_filename(__name__, - "templates/main_component.html") - with open(fileName, 'r') as templateFile: - self.componentTemplate = templateFile.read() - - fileName = \ - pkg_resources.resource_filename(__name__, - "templates/config.html") - with open(fileName, 'r') as templateFile: - self.configTemplate = templateFile.read() + package = 'mpas_analysis.shared.html.templates' + templates = {} + for name in ['main_page', 'main_component', 'config']: + resource = resources.files(package).joinpath(f'{name}.html') + with resource.open('r') as templateFile: + templates[name] = templateFile.read() + self.pageTemplate = templates['main_page'] + self.componentTemplate = templates['main_component'] + self.configTemplate = templates['config'] # start with no components self.components = OrderedDict() @@ -273,45 +264,34 @@ def generate(self): pageText = _replace_tempate_text(self.pageTemplate, replacements) - htmlBaseDirectory = build_config_full_path(self.config, 'output', - 'htmlSubdirectory') + htmlBaseDirectory = build_config_full_path( + self.config, 'output', 'htmlSubdirectory' + ) for subdir in ['css', 'js']: - try: - makedirs('{}/{}'.format(htmlBaseDirectory, subdir)) - except OSError: - pass + makedirs(f'{htmlBaseDirectory}/{subdir}', exist_ok=True) - outFileName = '{}/index.html'.format(htmlBaseDirectory) + outFileName = f'{htmlBaseDirectory}/index.html' with open(outFileName, mode='w') as mainFile: mainFile.write( - pageText.encode('ascii', - 'xmlcharrefreplace').decode('ascii')) + pageText.encode('ascii', 'xmlcharrefreplace').decode('ascii') + ) # copy the css and js files as well as general images - fileName = \ - pkg_resources.resource_filename(__name__, - "templates/style.css") - copyfile(fileName, '{}/css/style.css'.format(htmlBaseDirectory)) - - fileName = \ - pkg_resources.resource_filename(__name__, - "templates/index.js") - copyfile(fileName, '{}/js/index.js'.format(htmlBaseDirectory)) - - fileName = \ - pkg_resources.resource_filename(__name__, - "templates/mpas_logo.png") - copyfile(fileName, '{}/mpas_logo.png'.format(htmlBaseDirectory)) - - fileName = \ - pkg_resources.resource_filename(__name__, - "templates/config.png") - copyfile(fileName, '{}/config.png'.format(htmlBaseDirectory)) - - with open('{}/complete.{}.cfg'.format(htmlBaseDirectory, - runName), 'w') as configFile: + resource_targets = [ + ("style.css", "css/style.css"), + ("index.js", "js/index.js"), + ("mpas_logo.png", "mpas_logo.png"), + ("config.png", "config.png"), + ] + for resource_name, target_path in resource_targets: + package = 'mpas_analysis.shared.html.templates' + fileName = resources.files(package).joinpath(resource_name) + copyfile(str(fileName), f'{htmlBaseDirectory}/{target_path}') + + outFileName = f'{htmlBaseDirectory}/complete.{runName}.cfg' + with open(outFileName, 'w') as configFile: self.config.write(configFile) @@ -386,11 +366,10 @@ def __init__(self, config, name, subdirectory, controlConfig=None): for templateName in ['page', 'quicklink', 'group', 'gallery', 'image', 'subtitle']: # get template text - fileName = pkg_resources.resource_filename( - __name__, - "templates/component_{}.html".format(templateName)) - - with open(fileName, 'r') as templateFile: + package = 'mpas_analysis.shared.html.templates' + resource = resources.files(package).joinpath( + f'component_{templateName}.html') + with resource.open('r') as templateFile: self.templates[templateName] = templateFile.read() # start with no groups diff --git a/mpas_analysis/shared/html/templates/__init__.py b/mpas_analysis/shared/html/templates/__init__.py new file mode 100644 index 000000000..e69de29bb From 49635b47f7f712c9ef09ca282d0c982cf64ca206 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 12 Jun 2025 04:06:15 -0500 Subject: [PATCH 047/116] Fix docstring for ClimatologyMapCustom --- mpas_analysis/ocean/climatology_map_custom.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mpas_analysis/ocean/climatology_map_custom.py b/mpas_analysis/ocean/climatology_map_custom.py index 7b6aa248c..f2c705268 100644 --- a/mpas_analysis/ocean/climatology_map_custom.py +++ b/mpas_analysis/ocean/climatology_map_custom.py @@ -23,12 +23,14 @@ class ClimatologyMapCustom(AnalysisTask): """ - A felxible analysis task for plotting climatologies of any MPAS-Ocean field + A flexible analysis task for plotting climatologies of any MPAS-Ocean field on cells from timeSeriesStatsMonthly at various depths (if the field has - vertical levels) and for various seasons. Various derived fields are also - supported: - * velocity magnitude - * thermal forcing (temperature - freezing temperature) + vertical levels) and for various seasons. + + Various derived fields are also supported: + + * velocity magnitude + * thermal forcing (temperature - freezing temperature) """ def __init__(self, config, mpasClimatologyTask, controlConfig=None): From b992832b79abc3bce176c74ba787d0800ffe4ce2 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 13 Jun 2025 03:30:40 -0500 Subject: [PATCH 048/116] Remap 2D and 3D custom climatologies with same subtask --- mpas_analysis/ocean/climatology_map_custom.py | 45 +++++-------------- 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/mpas_analysis/ocean/climatology_map_custom.py b/mpas_analysis/ocean/climatology_map_custom.py index f2c705268..14a700875 100644 --- a/mpas_analysis/ocean/climatology_map_custom.py +++ b/mpas_analysis/ocean/climatology_map_custom.py @@ -78,12 +78,6 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): # we assume variables have depth unless otherwise specified variables[varName]['has_depth'] = True - variables3D = {varName: variables[varName] for varName in - variables if variables[varName]['has_depth']} - - variableList2D = [variables[varName]['mpas'][0] for varName in - variables if not variables[varName]['has_depth']] - # read in what seasons we want to plot seasons = config.getexpression(sectionName, 'seasons') @@ -104,29 +98,14 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): raise ValueError(f'config section {sectionName} does not ' f'contain valid list of depths') - if variables3D: - remapMpasSubtask3D = RemapMpasDerivedVariableClimatology( - mpasClimatologyTask=mpasClimatologyTask, - parentTask=self, - climatologyName='custom3D', - variables=variables3D, - seasons=seasons, - depths=depths, - comparisonGridNames=comparisonGridNames) - else: - remapMpasSubtask3D = None - - if len(variableList2D) > 0: - remapMpasSubtask2D = RemapMpasClimatologySubtask( - mpasClimatologyTask=mpasClimatologyTask, - parentTask=self, - climatologyName='custom2D', - variableList=variableList2D, - seasons=seasons, - comparisonGridNames=comparisonGridNames, - subtaskName='remap2DVariables') - else: - remapMpasSubtask2D = None + remapMpasSubtask = RemapMpasDerivedVariableClimatology( + mpasClimatologyTask=mpasClimatologyTask, + parentTask=self, + climatologyName='custom3D', + variables=variables, + seasons=seasons, + depths=depths, + comparisonGridNames=comparisonGridNames) galleryGroup = 'Custom Climatology Maps' groupLink = 'custclimmaps' @@ -149,12 +128,8 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): if hasDepth: localDepths = depths - remapMpasSubtask = remapMpasSubtask3D - mpasVarName = varName else: localDepths = [None] - remapMpasSubtask = remapMpasSubtask2D - mpasVarName = metadata['mpas'][0] for comparisonGridName in comparisonGridNames: for depth in localDepths: @@ -178,8 +153,8 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): subtask.set_plot_info( outFileLabel=f'cust_{varName}', fieldNameInTitle=title, - mpasFieldName=mpasVarName, - refFieldName=mpasVarName, + mpasFieldName=varName, + refFieldName=varName, refTitleLabel=refTitleLabel, diffTitleLabel=diffTitleLabel, unitsLabel=units, From faa7120cbd434295cc336df493b80545cb620e14 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 14 Jun 2025 14:09:08 -0500 Subject: [PATCH 049/116] Update to mpas_tools >=1.2.2 --- ci/recipe/meta.yaml | 2 +- dev-spec.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/recipe/meta.yaml b/ci/recipe/meta.yaml index f1b412d0f..b68094c6f 100644 --- a/ci/recipe/meta.yaml +++ b/ci/recipe/meta.yaml @@ -32,7 +32,7 @@ requirements: - lxml - mache >=1.11.0 - matplotlib-base >=3.9.0 - - mpas_tools >=1.1.0,<2.0.0 + - mpas_tools >=1.2.2,<2.0.0 - nco >=4.8.1,!=5.2.6 - netcdf4 - numpy >=2.0,<3.0 diff --git a/dev-spec.txt b/dev-spec.txt index 1ce19783b..a414ed9e7 100644 --- a/dev-spec.txt +++ b/dev-spec.txt @@ -15,7 +15,7 @@ gsw lxml mache >=1.11.0 matplotlib-base>=3.9.0 -mpas_tools >=1.1.0,<2.0.0 +mpas_tools >=1.2.2,<2.0.0 nco>=4.8.1,!=5.2.6 netcdf4 numpy>=2.0,<3.0 From 16badff0c592c697cae77ba7246a6f56e4d6bbd5 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 14 Jun 2025 14:09:30 -0500 Subject: [PATCH 050/116] Update BSF climatology map to use syntax for latest mpas_tools --- mpas_analysis/ocean/climatology_map_bsf.py | 30 +++++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/mpas_analysis/ocean/climatology_map_bsf.py b/mpas_analysis/ocean/climatology_map_bsf.py index b385d99fe..5cd36ba3a 100644 --- a/mpas_analysis/ocean/climatology_map_bsf.py +++ b/mpas_analysis/ocean/climatology_map_bsf.py @@ -12,7 +12,7 @@ import xarray as xr -from mpas_tools.ocean.barotropic_streamfunction import ( +from mpas_tools.ocean import ( compute_barotropic_streamfunction, shift_barotropic_streamfunction ) @@ -216,7 +216,7 @@ def __init__(self, mpas_climatology_task, parent_task, variable_list, seasons, comparison_grid_names, subtaskName=subtask_name, vertices=True) - # this reequires a lot of memory so let's reserve all the available + # this requires a lot of memory so let's reserve all the available # tasks parallelTaskCount = self.config.getint('execute', 'parallelTaskCount') self.subprocessCount = parallelTaskCount @@ -316,11 +316,26 @@ def customize_masked_climatology(self, climatology, season): config = self.config ds_mesh = xr.open_dataset(self.restartFileName) - ds_mesh = ds_mesh[['cellsOnEdge', 'cellsOnVertex', 'nEdgesOnCell', - 'edgesOnCell', 'verticesOnCell', 'verticesOnEdge', - 'edgesOnVertex', 'dcEdge', 'dvEdge', 'bottomDepth', - 'maxLevelCell', 'latVertex', 'areaTriangle',]] - ds_mesh.load() + var_list = [ + 'cellsOnEdge', + 'cellsOnVertex', + 'nEdgesOnCell', + 'edgesOnCell', + 'verticesOnCell', + 'verticesOnEdge', + 'edgesOnVertex', + 'dcEdge', + 'dvEdge', + 'bottomDepth', + 'minLevelCell', + 'maxLevelCell', + 'latVertex', + 'areaTriangle', + ] + ds_mesh = ds_mesh[var_list].as_numpy() + + masked_filename = self.get_masked_file_name(season) + masked_dir = os.path.dirname(masked_filename) cells_on_vertex = ds_mesh.cellsOnVertex - 1 lat_vertex = ds_mesh.latVertex @@ -332,6 +347,7 @@ def customize_masked_climatology(self, climatology, season): include_bolus=self.include_bolus, include_submesoscale=self.include_submesoscale, logger=logger, + tmp_dir=masked_dir, ) lat_range = config.getexpression( From 1d05067e41af26bbacbcc92c71bb2be0593d8e5e Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Wed, 28 May 2025 15:30:27 -0700 Subject: [PATCH 051/116] Switch from SMV to dropdown in CI --- .github/workflows/build_workflow.yml | 5 +---- .github/workflows/docs_workflow.yml | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build_workflow.yml b/.github/workflows/build_workflow.yml index e622b364c..498ca7d9b 100644 --- a/.github/workflows/build_workflow.yml +++ b/.github/workflows/build_workflow.yml @@ -85,8 +85,5 @@ jobs: - if: ${{ steps.skip_check.outputs.should_skip != 'true' }} name: Build Sphinx Docs run: | - # sphinx-multiversion expects at least a "main" branch - git branch main || echo "branch main already exists." cd docs - sphinx-multiversion . _build/html - + DOCS_VERSION=test make versioned-html diff --git a/.github/workflows/docs_workflow.yml b/.github/workflows/docs_workflow.yml index 27ffdd2da..6bab1830b 100644 --- a/.github/workflows/docs_workflow.yml +++ b/.github/workflows/docs_workflow.yml @@ -62,7 +62,7 @@ jobs: pip check mpas_analysis sync diags --help cd docs - sphinx-multiversion . _build/html + DOCS_VERSION=${{ github.ref_name }} make versioned-html - name: Copy Docs and Commit run: | set -e @@ -71,17 +71,26 @@ jobs: cd docs # gh-pages branch must already exist git clone https://github.com/MPAS-Dev/MPAS-Analysis.git --branch gh-pages --single-branch gh-pages + + # Only replace docs in a directory with the destination branch name with latest changes. Docs for + # releases should be untouched. + rm -rf gh-pages/${{ github.ref_name }} + + # don't clobber existing release versions (in case we retroactively fixed them) + cp -r _build/html/${{ github.ref_name }} gh-pages/ + + mkdir -p gh-pages/shared + cp shared/version-switcher.js gh-pages/shared/version-switcher.js + + # Update the list of versions with all versions in the gh-pages directory. + python generate_versions_json.py + # Make sure we're in the gh-pages directory. cd gh-pages # Create `.nojekyll` (if it doesn't already exist) for proper GH Pages configuration. touch .nojekyll # Add `index.html` to point to the `develop` branch automatically. printf '' > index.html - # Only replace docs in a directory with the destination branch name with latest changes. Docs for - # releases should be untouched. - rm -rf ${{ github.head_ref || github.ref_name }} - # don't clobber existing release versions (in case we retroactively fixed them) - cp -r -n ../_build/html/* . # Configure git using GitHub Actions credentials. git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" From 2a7e9f5b2f671bd83d6479fd998ae83d273f35dd Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Wed, 28 May 2025 15:30:44 -0700 Subject: [PATCH 052/116] Remove SMV from dev spec --- dev-spec.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/dev-spec.txt b/dev-spec.txt index a414ed9e7..f06e30339 100644 --- a/dev-spec.txt +++ b/dev-spec.txt @@ -41,5 +41,4 @@ m2r2>=0.3.3 mistune<2 sphinx sphinx_rtd_theme -sphinx-multiversion tabulate From d3e235266349568cc1181a65923256d15b375bf5 Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Wed, 28 May 2025 15:31:02 -0700 Subject: [PATCH 053/116] Edit Makefile to handle version dropdown --- docs/Makefile | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/Makefile b/docs/Makefile index 82663109b..6cf495011 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -8,6 +8,23 @@ SPHINXPROJ = MPAS-Analysis SOURCEDIR = . BUILDDIR = _build +# Build into a versioned subdirectory +versioned-html: + @echo "Building version: $(DOCS_VERSION)" + $(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)/html/$(DOCS_VERSION)" + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html/$(DOCS_VERSION)." + @echo "Setting up shared version switcher for local preview..." + mkdir -p _build/html/shared + cp shared/version-switcher.js _build/html/shared/version-switcher.js + python generate_versions_json.py --local + +# Override html target to include local setup +html: + $(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)/html" + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + + # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) @@ -16,6 +33,10 @@ clean: rm -rf users_guide/*obs_table.rst developers_guide/generated users_guide/obs @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +clean-versioned-html: + rm -rf $(BUILDDIR)/html/* + @echo "Cleaned versioned HTML builds." + .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new From 2037685a3f71c039c29add502fc7d52b6b565a1a Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Mon, 2 Jun 2025 11:20:59 -0700 Subject: [PATCH 054/116] Remove old versioning template --- docs/_templates/versions.html | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 docs/_templates/versions.html diff --git a/docs/_templates/versions.html b/docs/_templates/versions.html deleted file mode 100644 index 625a9a384..000000000 --- a/docs/_templates/versions.html +++ /dev/null @@ -1,28 +0,0 @@ -{%- if current_version %} -

- - Other Versions - v: {{ current_version.name }} - - -
- {%- if versions.tags %} -
-
Tags
- {%- for item in versions.tags %} -
{{ item.name }}
- {%- endfor %} -
- {%- endif %} - {%- if versions.branches %} -
-
Branches
- {%- for item in versions.branches %} -
{{ item.name }}
- {%- endfor %} -
- {%- endif %} -
-
-{%- endif %} - From 7d9ec1f749dd06e2db8f40cfd2d8688c33e0380c Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Mon, 2 Jun 2025 11:21:16 -0700 Subject: [PATCH 055/116] Remove smv in pyproj --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 674be4f7c..e663ea1fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,6 @@ docs = [ "mistune<2", "sphinx", "sphinx_rtd_theme", - "sphinx-multiversion", "tabulate", ] From 719952a9c1004961ff909e088850a53eab4538a0 Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Mon, 2 Jun 2025 11:21:45 -0700 Subject: [PATCH 056/116] Remove smv from docs conf --- docs/conf.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 3e1a4841e..607ca70ed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,7 +37,6 @@ # ones. extensions = [ 'sphinx_rtd_theme', - 'sphinx_multiversion', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx', @@ -222,14 +221,6 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] -html_sidebars = { - "**": [ - "versions.html", - ], +html_context = { + "current_version": os.getenv("DOCS_VERSION", "main"), } - -# -- Options sphinx-multiversion ------------------------------------------- -# Include tags like "tags/1.0.0" -- 1.7.2 doesn't build -smv_tag_whitelist = r'^(?!1.7.2)\d+\.\d+.\d+$' -smv_branch_whitelist = r'^(develop|main)$' -smv_remote_whitelist = 'origin' From 2dadfce17060d6d60ddd56ac806a2e45598f18d8 Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Mon, 2 Jun 2025 11:22:11 -0700 Subject: [PATCH 057/116] Add version switcher script and js --- docs/generate_versions_json.py | 69 +++++++++++++++++++++++++++++++++ docs/shared/version-switcher.js | 48 +++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 docs/generate_versions_json.py create mode 100644 docs/shared/version-switcher.js diff --git a/docs/generate_versions_json.py b/docs/generate_versions_json.py new file mode 100644 index 000000000..1ab13280e --- /dev/null +++ b/docs/generate_versions_json.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +import argparse +import json +import os +import re + + +def version_key(name): + """Key function for sorting versions.""" + match = re.match(r'^(\d+)\.(\d+)\.(\d+)$', name) + if match: + # Sort by major, minor, patch + return tuple(map(int, match.groups())) + return () + + +# Mode: local or production +parser = argparse.ArgumentParser( + description='Generate versions.json for MPAS Analysis documentation.') +parser.add_argument( + '--local', + action='store_true', + help='Generate versions.json for local build.' +) +args = parser.parse_args() +local = args.local +base_dir = '_build/html' if local else 'gh-pages' +shared_dir = os.path.join(base_dir, 'shared') + +entries = [] + +if not os.path.exists(base_dir) or not os.listdir(base_dir): + raise FileNotFoundError( + f"Base directory '{base_dir}' does not exist or is empty.") + +versions = os.listdir(base_dir) +numeric_versions = [] +non_numeric_versions = [] + +for version in versions: + # Check if it matches version pattern + if re.match(r'^\d+\.\d+\.\d+$', version): + numeric_versions.append(version) + else: + non_numeric_versions.append(version) + +# Sort numeric versions by major, minor, patch in descending order +numeric_versions.sort(key=version_key, reverse=True) +# Sort non-numeric versions alphabetically +non_numeric_versions.sort() + +# Combine the sorted lists +versions = non_numeric_versions + numeric_versions + +if 'main' in versions: + versions.insert(0, versions.pop(versions.index('main'))) + +for name in versions: + path = os.path.join(base_dir, name) + if os.path.isdir(path) and name not in ('shared', '.git'): + entries.append({ + 'version': name, + 'url': f'../{name}/' if local else f'/MPAS-Analysis/{name}/' + }) + +os.makedirs(shared_dir, exist_ok=True) +with open(os.path.join(shared_dir, 'versions.json'), 'w') as f: + json.dump(entries, f, indent=2) + diff --git a/docs/shared/version-switcher.js b/docs/shared/version-switcher.js new file mode 100644 index 000000000..17b2d9c8a --- /dev/null +++ b/docs/shared/version-switcher.js @@ -0,0 +1,48 @@ +(async function () { + const container = document.getElementById("version-switcher"); + if (!container) return; + + const metaVersion = document.querySelector('meta[name="doc-version"]'); + const currentVersion = metaVersion ? metaVersion.content : "unknown"; + console.log("Detected current version:", currentVersion); + + async function fetchVersions() { + try { + const scriptUrl = document.currentScript.src; + const basePath = scriptUrl.substring(0, scriptUrl.lastIndexOf('/') + 1); + const versionsUrl = basePath + "versions.json"; + + const res = await fetch(versionsUrl); + if (!res.ok) throw new Error(`Failed to load ${versionsUrl}`); + return await res.json(); + } catch (err) { + console.error("Could not load versions.json:", err); + return []; + } + } + + const versions = await fetchVersions(); + if (!versions.length) return; + + const select = document.createElement("select"); + select.style.marginLeft = "1em"; + select.onchange = () => { + window.location.href = select.value; + }; + + versions.forEach(({ version, url }) => { + const option = document.createElement("option"); + option.value = url; + option.textContent = version; + if (version === currentVersion) { + option.selected = true; + } + select.appendChild(option); + }); + + const label = document.createElement("label"); + label.textContent = "Version: "; + label.style.color = "white"; + label.appendChild(select); + container.appendChild(label); +})(); From e5a8e43e35f0b8860bf3424ddd8c829a13bab3c1 Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Fri, 13 Jun 2025 15:28:05 -0700 Subject: [PATCH 058/116] Add version switcher to css --- docs/_static/style.css | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/_static/style.css b/docs/_static/style.css index 6cbfde333..22d7aa0a4 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -2,3 +2,29 @@ max-width: 1200px !important; } +#version-switcher select { + background-color: #2980b9; + color: white; + border: none; + border-radius: 4px; + padding: 4px 30px 4px 10px; + font-size: 0.9em; + appearance: none; /* Remove default dropdown arrow */ + background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg fill='white' height='10' viewBox='0 0 24 24' width='10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 10px center; + background-size: 12px; + } + + #version-switcher select:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.4); + background-color: #2c89c4; /* slightly lighter blue on focus */ + } + + /* Selected item in the dropdown menu */ + #version-switcher option:checked { + background-color: #dddddd; /* for selected */ + color: black; + } + From 48ee7308825e21bde2103511c036b4a7556c628a Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Fri, 13 Jun 2025 15:28:22 -0700 Subject: [PATCH 059/116] Add layout template --- docs/_templates/layout.html | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index efc29758f..d430c7ad6 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -3,3 +3,26 @@ {% endblock %} + +{% block footer %} + {{ super() }} + + + + + + + + + + +{% endblock %} + From 192cee112506a87b14993139dec526d976e5d40b Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Fri, 13 Jun 2025 15:28:39 -0700 Subject: [PATCH 060/116] Update docs building in run suite --- suite/run_suite.bash | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/suite/run_suite.bash b/suite/run_suite.bash index 70114596a..964103efe 100755 --- a/suite/run_suite.bash +++ b/suite/run_suite.bash @@ -41,8 +41,7 @@ conda deactivate py=${main_py} conda activate test_mpas_analysis_py${py} cd docs -make clean -make html +DOCS_VERSION=test make clean versioned-html cd .. machine=$(python -c "from mache import discover_machine; print(discover_machine())") From f765724a369eaa0e05628cc01d4900787c91141a Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Fri, 13 Jun 2025 15:29:04 -0700 Subject: [PATCH 061/116] Update docs building in README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 94a6ea76d..a9a9281c1 100644 --- a/README.md +++ b/README.md @@ -294,8 +294,7 @@ developers". Then run: To generate the `sphinx` documentation, run: ``` cd docs -make clean -make html +DOCS_VERSION=test make clean versioned-html ``` The results can be viewed in your web browser by opening: ``` From 6c0c8596e6ec7ca4d5eecccd32ca74e6f42518c7 Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Mon, 16 Jun 2025 11:04:03 -0700 Subject: [PATCH 062/116] Update docs creation in run dev suite --- suite/run_dev_suite.bash | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/suite/run_dev_suite.bash b/suite/run_dev_suite.bash index a6962bd82..371666d09 100755 --- a/suite/run_dev_suite.bash +++ b/suite/run_dev_suite.bash @@ -14,8 +14,7 @@ branch=$(git symbolic-ref --short HEAD) # test building the docs conda activate ${env_name} cd docs -make clean -make html +DOCS_VERSION=test make clean versioned-html cd .. machine=$(python -c "from mache import discover_machine; print(discover_machine())") From 00cbd03ead3d76dda398639c7fff9f4547719dc3 Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Mon, 16 Jun 2025 11:16:41 -0700 Subject: [PATCH 063/116] Update docs building in tutorials --- docs/tutorials/dev_add_task.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/dev_add_task.rst b/docs/tutorials/dev_add_task.rst index a4b7474a5..fc84e82a6 100644 --- a/docs/tutorials/dev_add_task.rst +++ b/docs/tutorials/dev_add_task.rst @@ -1143,7 +1143,7 @@ With the ``mpas_analysis_dev`` environment activated, you can run: .. code-block:: bash cd docs - make clean html + DOCS_VERSION=test make clean versioned-html to build the docs locally in the ``_build/html`` subdirectory. When generating documentation on HPC machines, you will want to copy the html output to the From efa208e5076eeb960b1323c4067336a3331c7abb Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 4 Jul 2025 10:33:41 -0500 Subject: [PATCH 064/116] Add task for plotting maps of wind stress curl The wind stress curl is first computed on the native grid (at vertices) and then is remapped to comparison grids. The reconstruciton of the wind stress at edges is computed with a utility funciton `vector_cell_to_edge_isotropic()`, analogous to the subroutine with the same name in the MPAS framework. --- mpas_analysis/__main__.py | 5 + mpas_analysis/default.cfg | 31 +++ mpas_analysis/ocean/__init__.py | 4 + .../ocean/climatology_map_wind_stress_curl.py | 212 ++++++++++++++++++ mpas_analysis/ocean/utility.py | 136 ++++++++++- mpas_analysis/polar_regions.cfg | 9 + 6 files changed, 388 insertions(+), 9 deletions(-) create mode 100644 mpas_analysis/ocean/climatology_map_wind_stress_curl.py diff --git a/mpas_analysis/__main__.py b/mpas_analysis/__main__.py index 7ffcaef30..9d8f3250e 100644 --- a/mpas_analysis/__main__.py +++ b/mpas_analysis/__main__.py @@ -189,6 +189,11 @@ def build_analysis_list(config, controlConfig): analyses.append(ocean.ClimatologyMapCustom( config, oceanClimatologyTasks['avg'], controlConfig)) + + analyses.append(ocean.ClimatologyMapWindStressCurl( + config, oceanClimatologyTasks['avg'], controlConfig) + ) + analyses.append(ocean.ConservationTask( config, controlConfig)) diff --git a/mpas_analysis/default.cfg b/mpas_analysis/default.cfg index bdb6c6174..4596f72f8 100755 --- a/mpas_analysis/default.cfg +++ b/mpas_analysis/default.cfg @@ -1691,6 +1691,37 @@ makeTables = False iceShelvesInTable = [] +[climatologyMapWindStressCurl] +## options related to plotting horizontally remapped climatologies of +## wind stress curl against control model results + +# colormap for model/observations +colormapNameResult = cmo.curl +# whether the colormap is indexed or continuous +colormapTypeResult = continuous +# color indices into colormapName for filled contours +# the type of norm used in the colormap +normTypeResult = linear +# A dictionary with keywords for the norm +normArgsResult = {'vmin': -1e-6, 'vmax': 1e-6} + +# colormap for differences +colormapNameDifference = cmo.balance +# whether the colormap is indexed or continuous +colormapTypeDifference = continuous +# the type of norm used in the colormap +normTypeDifference = linear +# A dictionary with keywords for the norm +normArgsDifference = {'vmin': -2e-7, 'vmax': 2e-7} + +# Months or seasons to plot (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, +# Nov, Dec, JFM, AMJ, JAS, OND, ANN) +seasons = ['ANN'] + +# comparison grid(s) on which to plot analysis +comparisonGrids = ['latlon'] + + [timeSeriesAntarcticMelt] ## options related to plotting time series of melt below Antarctic ice shelves diff --git a/mpas_analysis/ocean/__init__.py b/mpas_analysis/ocean/__init__.py index a64f2f212..cdf68106a 100644 --- a/mpas_analysis/ocean/__init__.py +++ b/mpas_analysis/ocean/__init__.py @@ -29,6 +29,10 @@ ClimatologyMapCustom ) +from mpas_analysis.ocean.climatology_map_wind_stress_curl import ( + ClimatologyMapWindStressCurl +) + from mpas_analysis.ocean.conservation import ConservationTask from mpas_analysis.ocean.time_series_temperature_anomaly import \ diff --git a/mpas_analysis/ocean/climatology_map_wind_stress_curl.py b/mpas_analysis/ocean/climatology_map_wind_stress_curl.py new file mode 100644 index 000000000..a73e125b9 --- /dev/null +++ b/mpas_analysis/ocean/climatology_map_wind_stress_curl.py @@ -0,0 +1,212 @@ +# This software is open source software available under the BSD-3 license. +# +# Copyright (c) 2022 Triad National Security, LLC. All rights reserved. +# Copyright (c) 2022 Lawrence Livermore National Security, LLC. All rights +# reserved. +# Copyright (c) 2022 UT-Battelle, LLC. All rights reserved. +# +# Additional copyright and license information can be found in the LICENSE file +# distributed with this code, or at +# https://raw.githubusercontent.com/MPAS-Dev/MPAS-Analysis/main/LICENSE + +import xarray as xr + +from mpas_tools.ocean.streamfunction.vorticity import ( + compute_vertically_integrated_vorticity, +) + +from mpas_analysis.ocean.utility import ( + vector_cell_to_edge_isotropic, + vector_to_edge_normal, +) +from mpas_analysis.shared import AnalysisTask +from mpas_analysis.shared.climatology import RemapMpasClimatologySubtask +from mpas_analysis.shared.plot import PlotClimatologyMapSubtask + + +class ClimatologyMapWindStressCurl(AnalysisTask): + """ + An analysis task for computing and plotting maps of the wind stress curl. + """ + + def __init__(self, config, mpas_climatology_task, control_config=None): + """ + Construct the analysis task. + + Parameters + ---------- + config : mpas_tools.config.MpasConfigParser + Configuration options + + mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask + The task that produced the climatology to be remapped and plotted + + control_config : mpas_tools.config.MpasConfigParser, optional + Configuration options for a control run (if any) + """ # noqa: E501 + + field_name = 'windStressCurl' + super().__init__( + config=config, + taskName='climatologyMapWindStressCurl', + componentName='ocean', + tags=[ + 'climatology', 'horizontalMap', field_name, 'publicObs' + ], + ) + + section_name = self.taskName + + # read in what seasons we want to plot + seasons = config.getexpression(section_name, 'seasons') + if len(seasons) == 0: + raise ValueError( + f'config section {section_name} does not contain ' + f'valid list of seasons' + ) + + comparison_grid_names = config.getexpression( + section_name, 'comparisonGrids' + ) + + if len(comparison_grid_names) == 0: + raise ValueError( + f'config section {section_name} does not contain ' + f'valid list of comparison grids' + ) + + variable_list = list(RemapMpasWindStressCurl.VARIABLES) + remap_climatology_subtask = RemapMpasWindStressCurl( + mpasClimatologyTask=mpas_climatology_task, + parentTask=self, + climatologyName=field_name, + variableList=variable_list, + seasons=seasons, + comparisonGridNames=comparison_grid_names, + subtaskName='remapWindStressCurl', + vertices=True, + ) + + self.add_subtask(remap_climatology_subtask) + + out_file_label = field_name + field_title = 'Wind Stress Curl' + remap_observations_subtask = None + + mpas_field_name = field_name + if control_config is None: + ref_field_name = None + ref_title_label = None + diff_title_label = 'Model - Observations' + + else: + control_run_name = control_config.get('runs', 'mainRunName') + ref_field_name = mpas_field_name + ref_title_label = f'Control: {control_run_name}' + diff_title_label = 'Main - Control' + + for comparison_grid_name in comparison_grid_names: + for season in seasons: + # make a new subtask for this season and comparison grid + subtask_name = f'plot{season}_{comparison_grid_name}' + + subtask = PlotClimatologyMapSubtask( + self, season, comparison_grid_name, + remap_climatology_subtask, remap_observations_subtask, + controlConfig=control_config, subtaskName=subtask_name) + subtask.set_plot_info( + outFileLabel=out_file_label, + fieldNameInTitle=field_title, + mpasFieldName=mpas_field_name, + refFieldName=ref_field_name, + refTitleLabel=ref_title_label, + diffTitleLabel=diff_title_label, + unitsLabel=r'N m$^{-3}$ $s^{-1}$', + imageCaption=field_title, + galleryGroup='Wind Stress Curl', + groupSubtitle=None, + groupLink='wsc', + galleryName=None, + configSectionName=section_name) + + self.add_subtask(subtask) + + +class RemapMpasWindStressCurl(RemapMpasClimatologySubtask): + """ + A subtask for computing climatologies of the wind stress curl before + it gets remapped to a comparison grid. + """ + + VARIABLES = ( + 'timeMonthly_avg_windStressZonal', + 'timeMonthly_avg_windStressMeridional', + ) + + def setup_and_check(self): + """ + Add the variables needed for computing wind stress curl to the + climatology task + """ + super().setup_and_check() + + # Add the variables and seasons, now that we have the variable list + self.mpasClimatologyTask.add_variables( + list(self.VARIABLES), self.seasons + ) + + def customize_masked_climatology(self, climatology, season): + """ + Compute the wind stress curl and add it to the climatology. + + Parameters + ---------- + climatology : xarray.Dataset + the climatology data set + + season : str + The name of the season to be masked + + Returns + ------- + climatology : xarray.Dataset + the modified climatology data set + """ + logger = self.logger + + ds_mesh = xr.open_dataset(self.restartFileName) + var_list = [ + 'verticesOnEdge', + 'cellsOnVertex', + 'kiteAreasOnVertex', + 'angleEdge', + 'areaTriangle', + 'dcEdge', + 'edgesOnVertex', + 'verticesOnEdge', + 'latVertex', + ] + ds_mesh = ds_mesh[var_list] + + ws_zonal_cell = climatology['timeMonthly_avg_windStressZonal'] + ws_meridional_cell = ( + climatology['timeMonthly_avg_windStressMeridional'] + ) + ws_zonal_edge, ws_meridional_edge = vector_cell_to_edge_isotropic( + ds_mesh, ws_zonal_cell, ws_meridional_cell + ) + ws_normal_edge = vector_to_edge_normal( + ds_mesh, ws_zonal_edge, ws_meridional_edge + ) + + # despite the name, this is the curl operator + wind_sress_curl, _ = compute_vertically_integrated_vorticity( + ds_mesh, ws_normal_edge, logger + ) + climatology['windStressCurl'] = wind_sress_curl + climatology['windStressCurl'].attrs['units'] = 'N m-3 s-1' + + # drop the original wind stress variables + climatology = climatology.drop_vars(list(self.VARIABLES)) + + return climatology diff --git a/mpas_analysis/ocean/utility.py b/mpas_analysis/ocean/utility.py index 66f4ec299..a95eda409 100644 --- a/mpas_analysis/ocean/utility.py +++ b/mpas_analysis/ocean/utility.py @@ -15,8 +15,8 @@ # ------- # Xylar Asay-Davis -import numpy -import xarray +import numpy as np +import xarray as xr def add_standard_regions_and_subset(ds, config, regionShortNames=None): @@ -111,9 +111,12 @@ def compute_zmid(bottomDepth, maxLevelCell, layerThickness): nVertLevels = layerThickness.sizes['nVertLevels'] - vertIndex = \ - xarray.DataArray.from_dict({'dims': ('nVertLevels',), - 'data': numpy.arange(nVertLevels)}) + vertIndex = xr.DataArray.from_dict( + { + 'dims': ('nVertLevels',), + 'data': np.arange(nVertLevels) + } + ) layerThickness = layerThickness.where(vertIndex <= maxLevelCell) @@ -156,9 +159,12 @@ def compute_zinterface(bottomDepth, maxLevelCell, layerThickness): nVertLevels = layerThickness.sizes['nVertLevels'] - vertIndex = \ - xarray.DataArray.from_dict({'dims': ('nVertLevels',), - 'data': numpy.arange(nVertLevels)}) + vertIndex = xr.DataArray.from_dict( + { + 'dims': ('nVertLevels',), + 'data': np.arange(nVertLevels) + } + ) layerThickness = layerThickness.where(vertIndex <= maxLevelCell) thicknessSum = layerThickness.sum(dim='nVertLevels') @@ -174,6 +180,118 @@ def compute_zinterface(bottomDepth, maxLevelCell, layerThickness): zInterfaceList.append(zBot) zTop = zBot - zInterface = xarray.concat(zInterfaceList, dim='nVertLevelsP1').transpose( + zInterface = xr.concat(zInterfaceList, dim='nVertLevelsP1').transpose( 'nCells', 'nVertLevelsP1') return zInterface + + +def vector_cell_to_edge_isotropic(ds_mesh, zonal_cell, meridional_cell): + """ + Compute the zonal and meridional components of a vector at edges from + cell-centered components using isotropic area-weighted averaging. + + Parameters + ---------- + ds_mesh : xarray.Dataset + MPAS mesh variables, must include: + - verticesOnEdge + - cellsOnVertex + - kiteAreasOnVertex + + zonal_cell : xarray.DataArray + Zonal component at cell centers (nCells,) + + meridional_cell : xarray.DataArray + Meridional component at cell centers (nCells,) + + Returns + ------- + zonal_edge : xarray.DataArray + Zonal component at edges (nEdges,) + + meridional_edge : xarray.DataArray + Meridional component at edges (nEdges,) + """ + vertices_on_edge = ds_mesh.verticesOnEdge - 1 + cells_on_vertex = ds_mesh.cellsOnVertex - 1 + kite_areas_on_vertex = ds_mesh.kiteAreasOnVertex + + n_edges = vertices_on_edge.sizes['nEdges'] + vertex_degree = cells_on_vertex.sizes['vertexDegree'] + + zonal_edge = np.zeros(n_edges, dtype=float) + meridional_edge = np.zeros(n_edges, dtype=float) + area_sum = np.zeros(n_edges, dtype=float) + + for v in range(2): + # all valid edges have 2 valid vertices on that edge + voe = vertices_on_edge.isel(TWO=v) + for c in range(vertex_degree): + # cells on vertices on edge + covoe = cells_on_vertex.isel( + vertexDegree=c, + nVertices=voe + ) + valid = covoe >= 0 + valid_covoe = covoe.isel(nEdges=valid) + valid_voe = voe.isel(nEdges=valid) + area = kite_areas_on_vertex.isel( + vertexDegree=c, + nVertices=valid_voe + ).values + if np.any(area == 0): + raise ValueError( + "Some kite areas of valid cells on vertex have zero area. " + "This seems to be a bug in the mesh or " + "vector_cell_to_edge_isotropic()." + ) + zcell = zonal_cell.isel(nCells=valid_covoe).values + mcell = meridional_cell.isel(nCells=valid_covoe).values + zonal_edge[valid] += zcell * area + meridional_edge[valid] += mcell * area + area_sum[valid] += area + + if np.any(area_sum == 0): + raise ValueError( + "Some edges have zero area. This seems to be a bug in the mesh " + "or vector_cell_to_edge_isotropic()." + ) + + # Normalize by the area sum to get the average + zonal_edge /= area_sum + meridional_edge /= area_sum + + # Wrap as xarray DataArrays + zonal_edge = xr.DataArray(zonal_edge, dims=('nEdges',)) + meridional_edge = xr.DataArray(meridional_edge, dims=('nEdges',)) + return zonal_edge, meridional_edge + + +def vector_to_edge_normal(ds_mesh, zonal_edge, meridional_edge): + """ + Compute the normal component of a vector at an edge from + the zonal and meridional components. + + Parameters + ---------- + ds_mesh : xarray.Dataset + MPAS mesh variables, must include: + - angleEdge + + zonal_edge : xarray.DataArray + Zonal component at edges (nEdges,) + + meridional_edge : xarray.DataArray + Meridional component at edges (nEdges,) + + Returns + ------- + normal_edge : xarray.DataArray + Normal component at edges (nEdges,) + """ + + angle_edge = ds_mesh.angleEdge + normal_edge = ( + np.cos(angle_edge) * zonal_edge + np.sin(angle_edge) * meridional_edge + ) + return normal_edge diff --git a/mpas_analysis/polar_regions.cfg b/mpas_analysis/polar_regions.cfg index 96f6550ca..ef6fbfaa7 100644 --- a/mpas_analysis/polar_regions.cfg +++ b/mpas_analysis/polar_regions.cfg @@ -788,6 +788,15 @@ makeTables = True # ['all'] for all 106 ice shelves and regions. iceShelvesInTable = ['all'] + +[climatologyMapWindStressCurl] +## options related to plotting horizontally remapped climatologies of +## wind stress curl against control model results + +# comparison grid(s) on which to plot analysis +comparisonGrids = ['latlon', 'arctic_extended', 'antarctic_extended'] + + [timeSeriesTransport] ## options related to plotting time series of transport through transects transportGroups = ['Transport Transects', 'Arctic Transport Transects'] From 94b9e9822678aefe8766dc1233c8d894d3aa5db7 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 7 Jul 2025 20:53:09 +0200 Subject: [PATCH 065/116] Fix units --- mpas_analysis/ocean/climatology_map_wind_stress_curl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mpas_analysis/ocean/climatology_map_wind_stress_curl.py b/mpas_analysis/ocean/climatology_map_wind_stress_curl.py index a73e125b9..d79df1ff6 100644 --- a/mpas_analysis/ocean/climatology_map_wind_stress_curl.py +++ b/mpas_analysis/ocean/climatology_map_wind_stress_curl.py @@ -121,7 +121,7 @@ def __init__(self, config, mpas_climatology_task, control_config=None): refFieldName=ref_field_name, refTitleLabel=ref_title_label, diffTitleLabel=diff_title_label, - unitsLabel=r'N m$^{-3}$ $s^{-1}$', + unitsLabel=r'N m$^{-3}$', imageCaption=field_title, galleryGroup='Wind Stress Curl', groupSubtitle=None, @@ -204,7 +204,7 @@ def customize_masked_climatology(self, climatology, season): ds_mesh, ws_normal_edge, logger ) climatology['windStressCurl'] = wind_sress_curl - climatology['windStressCurl'].attrs['units'] = 'N m-3 s-1' + climatology['windStressCurl'].attrs['units'] = 'N m-3' # drop the original wind stress variables climatology = climatology.drop_vars(list(self.VARIABLES)) From 8cfeb053009873b1d776ae5396a76561be815be5 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 10 Jul 2025 04:24:24 -0500 Subject: [PATCH 066/116] Update the documentation --- docs/developers_guide/api.rst | 3 + docs/users_guide/analysis_tasks.rst | 1 + .../tasks/climatologyMapWindStressCurl.rst | 68 ++++++++++++++++++ .../tasks/examples/wind_stress_curl.png | Bin 0 -> 264236 bytes 4 files changed, 72 insertions(+) create mode 100644 docs/users_guide/tasks/climatologyMapWindStressCurl.rst create mode 100644 docs/users_guide/tasks/examples/wind_stress_curl.png diff --git a/docs/developers_guide/api.rst b/docs/developers_guide/api.rst index 3a93b1a59..56a2e2631 100644 --- a/docs/developers_guide/api.rst +++ b/docs/developers_guide/api.rst @@ -78,6 +78,7 @@ Ocean tasks ClimatologyMapArgoSalinity ClimatologyMapWaves ClimatologyMapCustom + ClimatologyMapWindStressCurl GeojsonNetcdfTransects IndexNino34 MeridionalHeatTransport @@ -132,6 +133,8 @@ Ocean utilities add_standard_regions_and_subset get_standard_region_names compute_zmid + compute_zinterface + vector_cell_to_edge_isotropic Sea ice tasks diff --git a/docs/users_guide/analysis_tasks.rst b/docs/users_guide/analysis_tasks.rst index 436b5a5f4..0d36779ad 100644 --- a/docs/users_guide/analysis_tasks.rst +++ b/docs/users_guide/analysis_tasks.rst @@ -23,6 +23,7 @@ Analysis Tasks tasks/climatologyMapSSH tasks/climatologyMapVel tasks/climatologyMapWaves + tasks/climatologyMapWindStressCurl tasks/climatologyMapWoa tasks/antshipTransects diff --git a/docs/users_guide/tasks/climatologyMapWindStressCurl.rst b/docs/users_guide/tasks/climatologyMapWindStressCurl.rst new file mode 100644 index 000000000..ac14b36e8 --- /dev/null +++ b/docs/users_guide/tasks/climatologyMapWindStressCurl.rst @@ -0,0 +1,68 @@ +.. _task_climatologyMapWindStressCurl: + +climatologyMapWindStressCurl +============================ + +An analysis task for plotting maps of wind stress curl. + +Component and Tags:: + + component: ocean + tags: climatology, horizontalMap, windStressCurl, publicObs + +Configuration Options +--------------------- + +The following configuration options are available for this task:: + + [climatologyMapWindStressCurl] + ## options related to plotting horizontally remapped climatologies of + ## wind stress curl against control model results + + # colormap for model/observations + colormapNameResult = cmo.curl + # whether the colormap is indexed or continuous + colormapTypeResult = continuous + # color indices into colormapName for filled contours + # the type of norm used in the colormap + normTypeResult = linear + # A dictionary with keywords for the norm + normArgsResult = {'vmin': -1e-6, 'vmax': 1e-6} + + # colormap for differences + colormapNameDifference = cmo.balance + # whether the colormap is indexed or continuous + colormapTypeDifference = continuous + # the type of norm used in the colormap + normTypeDifference = linear + # A dictionary with keywords for the norm + normArgsDifference = {'vmin': -2e-7, 'vmax': 2e-7} + + # Months or seasons to plot (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, + # Nov, Dec, JFM, AMJ, JAS, OND, ANN) + seasons = ['ANN'] + + # comparison grid(s) on which to plot analysis + comparisonGrids = ['latlon'] + +For analysis focused on polar regions (using the ``--polar_regions`` flag), +the following config options add Arctic and Antarctic comparison grids:: + + [climatologyMapWindStressCurl] + ## options related to plotting horizontally remapped climatologies of + ## wind stress curl against control model results + + # comparison grid(s) on which to plot analysis + comparisonGrids = ['latlon', 'arctic_extended', 'antarctic_extended'] + +For more details, see: + * :ref:`config_colormaps` + * :ref:`config_seasons` + * :ref:`config_comparison_grids` + +Example Result +-------------- + +.. image:: examples/wind_stress_curl.png + :width: 500 px + :align: center diff --git a/docs/users_guide/tasks/examples/wind_stress_curl.png b/docs/users_guide/tasks/examples/wind_stress_curl.png new file mode 100644 index 0000000000000000000000000000000000000000..8dd3b9f2710e3539b4444be46f94515bb30755e5 GIT binary patch literal 264236 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

>Ion9Roq1#C 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 Date: Fri, 11 Jul 2025 11:55:11 +0200 Subject: [PATCH 067/116] Flesh out the developer's guide This includes: * adding a quick start for developers * adding a note on building the documentation * documenting the test suites * notes on how to make releases --- docs/.gitignore | 2 +- docs/developers_guide/docs.rst | 32 ++++++ docs/developers_guide/quick_start.rst | 130 ++++++++++++++++++++++ docs/developers_guide/releasing.rst | 95 +++++++++++++++++ docs/developers_guide/test_suite.rst | 148 ++++++++++++++++++++++++++ docs/index.rst | 5 + 6 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 docs/developers_guide/docs.rst create mode 100644 docs/developers_guide/quick_start.rst create mode 100644 docs/developers_guide/releasing.rst create mode 100644 docs/developers_guide/test_suite.rst diff --git a/docs/.gitignore b/docs/.gitignore index 6290f34f4..9d852aad7 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -14,4 +14,4 @@ design_docs/remapper.rst design_docs/template.rst design_docs/timekeeping_reorg.rst design_docs/variable_mapping_reorg.rst -quick_start.rst +/quick_start.rst diff --git a/docs/developers_guide/docs.rst b/docs/developers_guide/docs.rst new file mode 100644 index 000000000..f211aee48 --- /dev/null +++ b/docs/developers_guide/docs.rst @@ -0,0 +1,32 @@ +Building the Documentation +========================== + +With the ``mpas_analysis_dev`` environment activated, you can run: + +.. code-block:: bash + + cd docs + DOCS_VERSION=test make clean versioned-html + +to build the docs locally in the ``_build/html`` subdirectory. + +The docs should build cleanly. If they don't, please attempt to fix the +errors and warnings even if they are not related to your changes. We want +to keep the documentation in good shape. + +Previewing the Documentation +---------------------------- + +When generating documentation on HPC machines, you will want to copy the html +output to the public web space to view it, or if the web portal is being +cranky, scp it to your local machine. + +To preview the documentation locally, open the ``index.html`` file in the +``_build/html/test`` directory with your browser or try: + +.. code-block:: bash + + cd _build/html + python -m http.server 8000 + +Then, open http://0.0.0.0:8000/test/ in your browser. diff --git a/docs/developers_guide/quick_start.rst b/docs/developers_guide/quick_start.rst new file mode 100644 index 000000000..8adf5cd14 --- /dev/null +++ b/docs/developers_guide/quick_start.rst @@ -0,0 +1,130 @@ +Quick Start for Developers +========================== + +This guide provides a condensed overview for developers to get started with +MPAS-Analysis development. + +1. Fork and Clone the Repository +-------------------------------- +- Fork `MPAS-Analysis `_ on GitHub. +- Clone the main repo and your fork locally: + - Create a base directory (e.g., ``mpas-analysis``). + - Clone the main repo: + + .. code-block:: bash + + git clone git@github.com:MPAS-Dev/MPAS-Analysis.git develop + + - Add your fork as a remote: + + .. code-block:: bash + + git remote add /MPAS-Analysis git@github.com:/MPAS-Analysis.git + +2. Configure Git +---------------- +- Set up your ``~/.gitconfig`` with your name and email (must match your + GitHub account). +- Recommended: set editor, color, and useful aliases. + +3. Set Up SSH Keys +------------------ +- Add SSH keys to GitHub for push access. +- See: https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account + +4. Create a Development Worktree +-------------------------------- +- Fetch latest changes: + + .. code-block:: bash + + git fetch --all -p + +- Create a worktree for your feature branch: + + .. code-block:: bash + + git worktree add ../ + +- Enter the worktree directory: + + .. code-block:: bash + + cd ../ + +5. Set Up Conda Environment +--------------------------- +- Install Miniforge3 (recommended) or Miniconda. +- For Miniconda, add ``conda-forge`` channel and set strict priority. +- Create environment: + + .. code-block:: bash + + conda create -y -n mpas_analysis_dev --file dev-spec.txt + +- Activate: + + .. code-block:: bash + + conda activate mpas_analysis_dev + +- Install MPAS-Analysis in edit mode: + + .. code-block:: bash + + python -m pip install --no-deps --no-build-isolation -e . + +6. Activate Environment (each session) +-------------------------------------- +- For bash: + + .. code-block:: bash + + source ~/miniforge3/etc/profile.d/conda.sh; conda activate mpas_analysis_dev + +- For csh: + + .. code-block:: csh + + source ~/miniforge3/etc/profile.d/conda.csh; conda activate mpas_analysis_dev + +7. Configure and Run MPAS-Analysis +---------------------------------- +- Copy and edit a config file (e.g., ``example_e3sm.cfg``) for your run. +- Set required options: ``mainRunName``, ``baseDirectory``, ``mpasMeshName``, output paths. +- Set ``mapMpiTasks = 1`` and ``mapParallelExec = None`` for development environments. +- Export HDF5 file locking variable if needed: + - Bash: + + .. code-block:: bash + + export HDF5_USE_FILE_LOCKING=FALSE + + - Csh: + + .. code-block:: csh + + setenv HDF5_USE_FILE_LOCKING FALSE + +- Run analysis: + + .. code-block:: bash + + mpas_analysis -m .cfg + +8. View Results +--------------- +- Output is a set of web pages in your specified output directory. +- On some systems, update permissions: + + .. code-block:: bash + + chmod -R ugo+rX + +- See the main web page for links to results and provenance info. + +Additional Recommendations +-------------------------- +- Use VS Code for remote editing and linting (optional). + +For more details, see the full :doc:`../tutorials/dev_getting_started`. diff --git a/docs/developers_guide/releasing.rst b/docs/developers_guide/releasing.rst new file mode 100644 index 000000000..26e4fb2e9 --- /dev/null +++ b/docs/developers_guide/releasing.rst @@ -0,0 +1,95 @@ +.. _dev_releasing: + +*********************** +Releasing a New Version +*********************** + +This document describes the steps for maintainers to tag and release a new +version of ``MPAS-Analysis``, and to update the conda-forge feedstock. + +Version Bump and Dependency Updates +=================================== + +1. **Update the Version Number** + + - Open a pull request (PR) to update the version number in the following + two files: + + - ``mpas_analysis/version.py`` + + - ``ci/recipe/meta.yaml`` + + - Make sure the version follows `semantic versioning `_. + +2. **Check and Update Dependencies** + + - Ensure that dependencies and their constraints are up-to-date and + consistent in: + + - ``ci/recipe/meta.yaml`` (dependencies for the conda-forge release) + + - ``pyproject.toml`` (dependencies for PyPI; used as a sanity check) + + - ``dev-spec.txt`` (development dependencies; should be a superset of + those for the conda-forge release) + + - The dependencies in ``meta.yaml`` are the ones that will be used for the + released package on conda-forge. The dependencies in ``pyproject.toml`` + are for PyPI and should be kept in sync as much as possible but are only + there as a sanity check when we run ``pip check``. The ``dev-spec.txt`` + file should include all dependencies needed for development and testing. + + - Review and update dependency versions and constraints as needed. + +3. **Make a PR and merge it** + +Tagging and Publishing a Release +================================ + +4. **Tag the Release on GitHub** + + - Go to https://github.com/MPAS-Dev/MPAS-Analysis/releases and click on + "Draft a new release". + + - Enter the appropriate tag for the release, following semantic versioning + (e.g., ``1.13.0``; **do not** include a ``v`` in front). + + - Enter a release title (typically the release version **with** a ``v`` in + front, e.g., ``v1.13.0``). + + - Write a description and/or use the "Generate release notes" button to + auto-generate release notes. + + - If the release is ready, click "Publish release". Otherwise, save it as a + draft. + +Updating the conda-forge Feedstock +================================== + +5. **Update the conda-forge Feedstock** + + - After the release is published, update and merge a PR for the new release + at the conda-forge feedstock: + https://github.com/conda-forge/mpas-analysis-feedstock + + - The conda-forge bot should automatically create a pull request for the + new version within a few hours to a day after the release. + + - Compare the dependencies in the new release to those in the previous + release and update the recipe as needed. To do this: + + - Find the most recent release at + https://github.com/MPAS-Dev/MPAS-Analysis/releases + + - Use the "Compare" feature to select the previous release. + + - Under "changed files", locate ``ci/recipe/meta.yaml`` to see + any dependency changes. + + - Review and update the feedstock PR as needed, then merge it. + + - If you are not already a maintainer of the feedstock, you can request to + be added by creating a new issue at + https://github.com/conda-forge/mpas-analysis-feedstock/issues, choosing + "Bot command", and putting + ``@conda-forge-admin, please add user @username`` as the subject. diff --git a/docs/developers_guide/test_suite.rst b/docs/developers_guide/test_suite.rst new file mode 100644 index 000000000..0372ff96b --- /dev/null +++ b/docs/developers_guide/test_suite.rst @@ -0,0 +1,148 @@ +Test Suite Infrastructure +========================= + +The `suite` directory provides a comprehensive infrastructure for testing +MPAS-Analysis on supported machines (Anvil, Chrysalis, Perlmutter-CPU, and +Compy). The suite is designed to ensure code changes do not introduce +unexpected results and to validate MPAS-Analysis in various environments. + +Overview of Test Scripts +------------------------ + +There are three main scripts for running the test suite: + +1. **run_dev_suite.bash** (Developer Testing) + + - Use this script after activating your development environment + (must be named `mpas_analysis_dev`). + + - It builds the documentation and runs a series of analysis tasks on output + from a low-resolution (QUwLI240) simulation. + + - Each task produces a web page with results, accessible via the web portal. + + - Example usage: + + .. code-block:: bash + + $ source ~/miniforge3/etc/profile.d/conda.sh + $ conda activate mpas_analysis_dev + $ ./suite/run_dev_suite.bash + + - After completion, check for successful web page generation, e.g.: + + .. code-block:: bash + + $ tail -n 3 chrysalis_test_suite/main_py3.11/mpas_analysis.o793058 + + The last lines should include: + + .. code-block:: none + + Generating webpage for viewing results... + Web page: https://web.lcrc.anl.gov/public/e3sm/diagnostic_output//analysis_testing/chrysalis//main_py3.11/ + + - To quickly identify unfinished or failed tasks: + + .. code-block:: bash + + $ grep -L "Web page:" chrysalis_test_suite/*/mpas_analysis.o* + + - Developers should run this suite manually on each pull request before + merging and link the results in the PR. + +2. **run_suite.bash** (Package Build & Test) + + - Use this script to build the MPAS-Analysis conda package and test it in + fresh environments. + + - It creates conda environments for multiple Python versions, runs tests, + builds documentation, and executes the analysis suite. + + - Recommended for more thorough validation, especially before releases. + + - Example usage: + + .. code-block:: bash + + $ ./suite/run_suite.bash + +3. **run_e3sm_unified_suite.bash** (E3SM-Unified Deployment Testing) + + - Used during test deployments of E3SM-Unified to verify MPAS-Analysis + works as expected within the deployment. + + - Typically run by E3SM-Unified maintainers during deployment testing. + + - Example usage: + + .. code-block:: bash + + $ ./suite/run_e3sm_unified_suite.bash + +Supported Machines +------------------ + +The suite is designed to run only on supported machines: + +- Anvil + +- Chrysalis + +- Perlmutter-CPU (`pm-cpu`) + +- Compy + +If you attempt to run the suite on an unsupported machine, you will receive an +error. + +Modifying the Test Suite +------------------------ + +Developers may need to update the suite for new requirements: + +- **Python Versions**: + + - The Python versions tested are defined in the scripts (e.g., + `main_py=3.11`, `alt_py=3.10`). + + - To test additional versions, add them to the relevant script variables and + loops. + +- **Adding New Machines**: + + - Update the machine detection logic in `suite/setup.py` and add appropriate + input/output paths for the new machine. + + - Ensure the new machine is supported in the scripts and the web portal + configuration. + +- **Adding/Modifying Tests**: + + - To add new tests, update the list of runs in the scripts and + provide corresponding config files in the `suite` directory. + + - New tests could change which analysis tasks are run, the configuration for + running tasks overall (e.g. how climatologies are computed), or how + individual tasks are configured (e.g. focused on polar regions vs. global) + +- **Changing Simulation Data**: + + - Update the simulation name and mesh in `suite/setup.py` if you wish to + test on different output. + +Best Practices +-------------- + +- Always run the test suite before merging a pull request. + +- Link the results web page in your PR for reviewers. + +- Use the quick check (`grep -L "Web page:" ...`) to ensure all tasks + completed successfully. + +- Update the suite scripts and configs as needed to keep pace with + MPAS-Analysis development. + +For more details, see the comments and documentation within each script and +config file in the `suite` directory. diff --git a/docs/index.rst b/docs/index.rst index 8f65f7b00..5e6688fe7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,6 +28,11 @@ used those components. :caption: Developer's guide :maxdepth: 2 + developers_guide/quick_start + developers_guide/docs + developers_guide/test_suite + developers_guide/releasing + developers_guide/api design_docs/index From d93a03cb567cd3f44d02ab5ace0e60777d0c7118 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 18 Jul 2025 15:40:55 +0200 Subject: [PATCH 068/116] Update the releasing documentation --- docs/developers_guide/releasing.rst | 106 ++++++++++++++++------------ 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/docs/developers_guide/releasing.rst b/docs/developers_guide/releasing.rst index 26e4fb2e9..6225f8aa0 100644 --- a/docs/developers_guide/releasing.rst +++ b/docs/developers_guide/releasing.rst @@ -12,84 +12,96 @@ Version Bump and Dependency Updates 1. **Update the Version Number** - - Open a pull request (PR) to update the version number in the following - two files: + - Manually update the version number in the following files: - ``mpas_analysis/version.py`` - - ``ci/recipe/meta.yaml`` - Make sure the version follows `semantic versioning `_. + For release candidates, use versions like ``1.3.0rc1``. 2. **Check and Update Dependencies** - Ensure that dependencies and their constraints are up-to-date and consistent in: - - ``ci/recipe/meta.yaml`` (dependencies for the conda-forge release) - - - ``pyproject.toml`` (dependencies for PyPI; used as a sanity check) - - - ``dev-spec.txt`` (development dependencies; should be a superset of - those for the conda-forge release) + - ``ci/recipe/meta.yaml`` (for the conda-forge release) + - ``pyproject.toml`` (for PyPI; used for sanity checks) + - ``dev-spec.txt`` (development dependencies; should be a superset) - - The dependencies in ``meta.yaml`` are the ones that will be used for the - released package on conda-forge. The dependencies in ``pyproject.toml`` - are for PyPI and should be kept in sync as much as possible but are only - there as a sanity check when we run ``pip check``. The ``dev-spec.txt`` - file should include all dependencies needed for development and testing. - - - Review and update dependency versions and constraints as needed. + - Use the GitHub "Compare" feature to check for dependency changes between releases: + https://github.com/MPAS-Dev/MPAS-Analysis/compare 3. **Make a PR and merge it** + - Open a PR for the version bump and dependency changes and merge once + approved. + Tagging and Publishing a Release ================================ 4. **Tag the Release on GitHub** - - Go to https://github.com/MPAS-Dev/MPAS-Analysis/releases and click on - "Draft a new release". + - Go to https://github.com/MPAS-Dev/MPAS-Analysis/releases + - Click "Draft a new release" + - Enter a tag: + - For stable releases: ``1.3.0`` + - For release candidates: ``1.3.0rc1`` + - Set the release title to the version prefixed with ``v`` (e.g., + ``v1.3.0``) + - Generate or manually write release notes + - Mark as a pre-release if applicable + - Click "Publish release" - - Enter the appropriate tag for the release, following semantic versioning - (e.g., ``1.13.0``; **do not** include a ``v`` in front). +Updating the conda-forge Feedstock +================================== - - Enter a release title (typically the release version **with** a ``v`` in - front, e.g., ``v1.13.0``). +5. **Automatic Feedstock Update (Preferred Method)** - - Write a description and/or use the "Generate release notes" button to - auto-generate release notes. + - Wait for the ``regro-cf-autotick-bot`` to open a PR at: + https://github.com/conda-forge/mpas-analysis-feedstock - - If the release is ready, click "Publish release". Otherwise, save it as a - draft. + - This may take several hours to a day. -Updating the conda-forge Feedstock -================================== + - Review the PR: + - Confirm the version bump and dependency changes + - Merge once CI checks pass -5. **Update the conda-forge Feedstock** +6. **Manual Feedstock Update (Fallback Method)** - - After the release is published, update and merge a PR for the new release - at the conda-forge feedstock: - https://github.com/conda-forge/mpas-analysis-feedstock + If the bot PR does not appear or is too slow, update manually: + + - Download the release tarball: + + :: + + wget https://github.com/MPAS-Dev/MPAS-Analysis/archive/refs/tags/.tar.gz + + - Compute the SHA256 checksum: + + :: + + shasum -a 256 .tar.gz + + - In the ``meta.yaml`` of the feedstock recipe: + - Set ``{% set version = "" %}`` + - Set the new ``sha256`` value + - Update dependencies if needed + + - Commit, push to a new branch, and open a PR against the feedstock + - Follow any instructions in the PR template and merge once approved - - The conda-forge bot should automatically create a pull request for the - new version within a few hours to a day after the release. +Post Release Actions +==================== - - Compare the dependencies in the new release to those in the previous - release and update the recipe as needed. To do this: +7. **Verify and Announce** - - Find the most recent release at - https://github.com/MPAS-Dev/MPAS-Analysis/releases + - Install the package in a clean environment to test: - - Use the "Compare" feature to select the previous release. + :: - - Under "changed files", locate ``ci/recipe/meta.yaml`` to see - any dependency changes. + conda create -n test-mpas -c conda-forge mpas-analysis= - - Review and update the feedstock PR as needed, then merge it. + - Optionally announce the release on relevant communication channels - - If you are not already a maintainer of the feedstock, you can request to - be added by creating a new issue at - https://github.com/conda-forge/mpas-analysis-feedstock/issues, choosing - "Bot command", and putting - ``@conda-forge-admin, please add user @username`` as the subject. + - Update any documentation or release notes as needed From 91a42109bc23196634515411e976d4ac17032a1b Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 21 Jul 2025 12:59:39 +0200 Subject: [PATCH 069/116] Fix release notes We don't want people making release pages for release candidates on this repo. This merge includes a bit of other clean up as well. --- docs/developers_guide/releasing.rst | 63 ++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/docs/developers_guide/releasing.rst b/docs/developers_guide/releasing.rst index 6225f8aa0..b3c8e3ed5 100644 --- a/docs/developers_guide/releasing.rst +++ b/docs/developers_guide/releasing.rst @@ -26,7 +26,9 @@ Version Bump and Dependency Updates consistent in: - ``ci/recipe/meta.yaml`` (for the conda-forge release) + - ``pyproject.toml`` (for PyPI; used for sanity checks) + - ``dev-spec.txt`` (development dependencies; should be a superset) - Use the GitHub "Compare" feature to check for dependency changes between releases: @@ -40,23 +42,51 @@ Version Bump and Dependency Updates Tagging and Publishing a Release ================================ -4. **Tag the Release on GitHub** +4. **Tagging a Release Candidate** + + - For release candidates, **do not create a GitHub release page**. Just + create a tag from the command line: + + - Make sure your changes are merged into ``develop`` or your own update + branch (e.g. ``update-to-1.3.0``) and your local repo is up to date. + + - Tag the release candidate (e.g., ``1.3.0rc1``): + + :: + + git checkout develop + git fetch --all -p + git reset --hard origin/develop + git tag 1.3.0rc1 + git push origin 1.3.0rc1 + + (Replace ``1.3.0rc1`` with your actual version, and ``develop`` with + your branch if needed.) + + **Note:** This will only create a tag. No release page will be created + on GitHub. - - Go to https://github.com/MPAS-Dev/MPAS-Analysis/releases - - Click "Draft a new release" - - Enter a tag: - - For stable releases: ``1.3.0`` - - For release candidates: ``1.3.0rc1`` - - Set the release title to the version prefixed with ``v`` (e.g., - ``v1.3.0``) - - Generate or manually write release notes - - Mark as a pre-release if applicable - - Click "Publish release" +5. **Publishing a Stable Release** + + - For stable releases, create a GitHub release page as follows: + + - Go to https://github.com/MPAS-Dev/pyremap/releases + + - Click "Draft a new release" + + - Enter a tag (e.g., ``1.3.0``) + + - Set the release title to the version prefixed with ``v`` (e.g., + ``v1.3.0``) + + - Generate or manually write release notes + + - Click "Publish release" Updating the conda-forge Feedstock ================================== -5. **Automatic Feedstock Update (Preferred Method)** +6. **Automatic Feedstock Update (Preferred Method)** - Wait for the ``regro-cf-autotick-bot`` to open a PR at: https://github.com/conda-forge/mpas-analysis-feedstock @@ -67,7 +97,12 @@ Updating the conda-forge Feedstock - Confirm the version bump and dependency changes - Merge once CI checks pass -6. **Manual Feedstock Update (Fallback Method)** + **Note:** If you are impatient, you can accellerate this process by creating + a bot issue at: https://github.com/conda-forge/mpas-analysis-feedstock/issues + with the subject ``@conda-forge-admin, please update version``. This + will open a new PR with the version within a few minutes. + +7. **Manual Feedstock Update (Fallback Method)** If the bot PR does not appear or is too slow, update manually: @@ -94,7 +129,7 @@ Updating the conda-forge Feedstock Post Release Actions ==================== -7. **Verify and Announce** +8. **Verify and Announce** - Install the package in a clean environment to test: From 76a037e1ebef514547c5d6fcdf8e997874f147ca Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 28 Jul 2025 08:51:27 +0200 Subject: [PATCH 070/116] Fix docs for release candidates --- docs/developers_guide/releasing.rst | 79 +++++++++++++++++------------ 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/docs/developers_guide/releasing.rst b/docs/developers_guide/releasing.rst index b3c8e3ed5..4714cbde4 100644 --- a/docs/developers_guide/releasing.rst +++ b/docs/developers_guide/releasing.rst @@ -31,7 +31,8 @@ Version Bump and Dependency Updates - ``dev-spec.txt`` (development dependencies; should be a superset) - - Use the GitHub "Compare" feature to check for dependency changes between releases: + - Use the GitHub "Compare" feature to check for dependency changes between + releases: https://github.com/MPAS-Dev/MPAS-Analysis/compare 3. **Make a PR and merge it** @@ -39,8 +40,8 @@ Version Bump and Dependency Updates - Open a PR for the version bump and dependency changes and merge once approved. -Tagging and Publishing a Release -================================ +Tagging and Publishing a Release Candidate +========================================== 4. **Tagging a Release Candidate** @@ -66,11 +67,46 @@ Tagging and Publishing a Release **Note:** This will only create a tag. No release page will be created on GitHub. -5. **Publishing a Stable Release** +5. **Updating the conda-forge Feedstock for a Release Candidate** + + - The conda-forge feedstock does **not** update automatically for release + candidates. + - You must always create a PR manually, and it must target the ``dev`` + branch of the feedstock. + + Steps: + + - Download the release tarball: + + :: + + wget https://github.com/MPAS-Dev/MPAS-Analysis/archive/refs/tags/.tar.gz + + - Compute the SHA256 checksum: + + :: + + shasum -a 256 .tar.gz + + - In the ``meta.yaml`` of the feedstock recipe: + - Set ``{% set version = "" %}`` + - Set the new ``sha256`` value + - Update dependencies if needed + + - Commit, push to a new branch, and open a PR **against the ``dev`` branch** + of the feedstock: + https://github.com/conda-forge/mpas-analysis-feedstock + + - Follow any instructions in the PR template and merge once approved + +Releasing a Stable Version +========================== + +6. **Publishing a Stable Release** - For stable releases, create a GitHub release page as follows: - - Go to https://github.com/MPAS-Dev/pyremap/releases + - Go to https://github.com/MPAS-Dev/MPAS-Analysis/releases - Click "Draft a new release" @@ -83,10 +119,7 @@ Tagging and Publishing a Release - Click "Publish release" -Updating the conda-forge Feedstock -================================== - -6. **Automatic Feedstock Update (Preferred Method)** +7. **Updating the conda-forge Feedstock for a Stable Release** - Wait for the ``regro-cf-autotick-bot`` to open a PR at: https://github.com/conda-forge/mpas-analysis-feedstock @@ -97,34 +130,14 @@ Updating the conda-forge Feedstock - Confirm the version bump and dependency changes - Merge once CI checks pass - **Note:** If you are impatient, you can accellerate this process by creating + **Note:** If you are impatient, you can accelerate this process by creating a bot issue at: https://github.com/conda-forge/mpas-analysis-feedstock/issues with the subject ``@conda-forge-admin, please update version``. This will open a new PR with the version within a few minutes. -7. **Manual Feedstock Update (Fallback Method)** - - If the bot PR does not appear or is too slow, update manually: - - - Download the release tarball: - - :: - - wget https://github.com/MPAS-Dev/MPAS-Analysis/archive/refs/tags/.tar.gz - - - Compute the SHA256 checksum: - - :: - - shasum -a 256 .tar.gz - - - In the ``meta.yaml`` of the feedstock recipe: - - Set ``{% set version = "" %}`` - - Set the new ``sha256`` value - - Update dependencies if needed - - - Commit, push to a new branch, and open a PR against the feedstock - - Follow any instructions in the PR template and merge once approved + - If the bot PR does not appear or is too slow, you may update manually (see + the manual steps for release candidates above, but target the ``main`` + branch of the feedstock). Post Release Actions ==================== From 0918b3c4aae017522e82e3a7c82bd451a2ebde7c Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Thu, 7 Aug 2025 13:39:20 -0500 Subject: [PATCH 071/116] Add @andrewdnolan to dependabot reviewers --- .github/dependabot.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7bca37f8f..f12baee6f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,4 +12,5 @@ updates: reviewers: - "xylar" - "altheaden" + - "andrewdnolan" From 169e6ec6d92e718fba468497629aaeeede40f7ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:38:38 +0000 Subject: [PATCH 072/116] Bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build_workflow.yml | 2 +- .github/workflows/docs_workflow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_workflow.yml b/.github/workflows/build_workflow.yml index 498ca7d9b..6291110d7 100644 --- a/.github/workflows/build_workflow.yml +++ b/.github/workflows/build_workflow.yml @@ -37,7 +37,7 @@ jobs: paths_ignore: ${{ env.PATHS_IGNORE }} - if: ${{ steps.skip_check.outputs.should_skip != 'true' }} - uses: actions/checkout@v4 + uses: actions/checkout@v5 - if: ${{ steps.skip_check.outputs.should_skip != 'true' }} name: Cache Conda diff --git a/.github/workflows/docs_workflow.yml b/.github/workflows/docs_workflow.yml index 6bab1830b..b90bb7882 100644 --- a/.github/workflows/docs_workflow.yml +++ b/.github/workflows/docs_workflow.yml @@ -20,7 +20,7 @@ jobs: shell: bash -l {0} timeout-minutes: 20 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false fetch-depth: 0 From def17c78818b1350e155dfd406e579dd4af46755 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 15 Aug 2025 08:10:10 -0500 Subject: [PATCH 073/116] Update to mpas_tools >=1.3.0 --- ci/recipe/meta.yaml | 2 +- dev-spec.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/recipe/meta.yaml b/ci/recipe/meta.yaml index b68094c6f..700b3987d 100644 --- a/ci/recipe/meta.yaml +++ b/ci/recipe/meta.yaml @@ -32,7 +32,7 @@ requirements: - lxml - mache >=1.11.0 - matplotlib-base >=3.9.0 - - mpas_tools >=1.2.2,<2.0.0 + - mpas_tools >=1.3.0,<2.0.0 - nco >=4.8.1,!=5.2.6 - netcdf4 - numpy >=2.0,<3.0 diff --git a/dev-spec.txt b/dev-spec.txt index f06e30339..2f5b4b13e 100644 --- a/dev-spec.txt +++ b/dev-spec.txt @@ -15,7 +15,7 @@ gsw lxml mache >=1.11.0 matplotlib-base>=3.9.0 -mpas_tools >=1.2.2,<2.0.0 +mpas_tools >=1.3.0,<2.0.0 nco>=4.8.1,!=5.2.6 netcdf4 numpy>=2.0,<3.0 From fc2bc2f53293de6666f81d535bccc0e9d97f40fa Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 15 Aug 2025 10:10:18 -0500 Subject: [PATCH 074/116] Drop unlimited_dims if they aren't in the dataset --- mpas_analysis/shared/io/write_netcdf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mpas_analysis/shared/io/write_netcdf.py b/mpas_analysis/shared/io/write_netcdf.py index 99079083a..3347614d2 100644 --- a/mpas_analysis/shared/io/write_netcdf.py +++ b/mpas_analysis/shared/io/write_netcdf.py @@ -56,4 +56,10 @@ def write_netcdf_with_fill(ds, fileName, fillValues=netCDF4.default_fillvals): if dtype.type is numpy.bytes_: encodingDict[variableName] = {'dtype': str} + unlimited_dims = ds.encoding.get('unlimited_dims', None) + if unlimited_dims is not None: + if isinstance(unlimited_dims, str): + unlimited_dims = {unlimited_dims} + unlimited_dims = [dim for dim in unlimited_dims if dim in ds.dims] + ds.encoding['unlimited_dims'] = set(unlimited_dims) ds.to_netcdf(fileName, encoding=encodingDict) From 18c961bebb26f2d535995f7b6a773b4bf5c21b5b Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 8 Aug 2025 11:05:05 +0200 Subject: [PATCH 075/116] Switch from restart file to mesh file in all tasks E3SM will switch to using ADIOS format for restart files in some configurations. But the MPAS-Ocean and -seaice mesh files from the "mesh" stream should always be safe to use for mesh variables (including those for the 3D MPAS-Ocean mesh). --- .../ocean/climatology_map_antarctic_melt.py | 14 +++---- mpas_analysis/ocean/climatology_map_bsf.py | 2 +- mpas_analysis/ocean/climatology_map_custom.py | 8 ++-- .../ocean/climatology_map_ohc_anomaly.py | 12 +++--- .../ocean/climatology_map_wind_stress_curl.py | 2 +- .../ocean/compute_transects_subtask.py | 2 +- mpas_analysis/ocean/histogram.py | 18 +++++---- .../ocean/meridional_heat_transport.py | 12 +++--- .../ocean/ocean_regional_profiles.py | 27 ++++++------- mpas_analysis/ocean/plot_hovmoller_subtask.py | 14 ++++--- mpas_analysis/ocean/regional_ts_diagrams.py | 24 ++++++------ .../ocean/remap_depth_slices_subtask.py | 4 +- mpas_analysis/ocean/streamfunction_moc.py | 38 ++++++++++++------- .../ocean/time_series_antarctic_melt.py | 18 +++------ .../ocean/time_series_ocean_regions.py | 20 +++++----- .../ocean/time_series_ohc_anomaly.py | 17 +++++---- mpas_analysis/ocean/time_series_transport.py | 22 ++++++----- .../sea_ice/climatology_map_albedo.py | 4 +- .../sea_ice/climatology_map_area_pond.py | 4 +- .../sea_ice/climatology_map_area_ridge.py | 9 ++--- .../sea_ice/climatology_map_melting.py | 5 ++- .../sea_ice/climatology_map_production.py | 5 ++- .../sea_ice/climatology_map_snow_depth.py | 8 ++-- .../sea_ice/climatology_map_snowice.py | 4 +- .../sea_ice/climatology_map_snowmelt.py | 4 +- .../climatology_map_tendency_area_thermo.py | 9 +++-- .../climatology_map_tendency_area_transp.py | 9 +++-- .../climatology_map_tendency_volume_thermo.py | 9 +++-- .../climatology_map_tendency_volume_transp.py | 9 +++-- .../sea_ice/climatology_map_volume_ridge.py | 8 ++-- mpas_analysis/sea_ice/time_series.py | 11 +++--- .../remap_mpas_climatology_subtask.py | 17 +++++---- .../regions/compute_region_masks_subtask.py | 10 +++-- .../compute_transect_masks_subtask.py | 8 ++-- 34 files changed, 208 insertions(+), 179 deletions(-) diff --git a/mpas_analysis/ocean/climatology_map_antarctic_melt.py b/mpas_analysis/ocean/climatology_map_antarctic_melt.py index f3956cd9c..60fa7a20b 100644 --- a/mpas_analysis/ocean/climatology_map_antarctic_melt.py +++ b/mpas_analysis/ocean/climatology_map_antarctic_melt.py @@ -333,8 +333,8 @@ def run_task(self): # ------- # Xylar Asay-Davis - # first, load the land-ice mask from the restart file - dsLandIceMask = xr.open_dataset(self.restartFileName) + # first, load the land-ice mask from the mesh file + dsLandIceMask = xr.open_dataset(self.meshFilename) dsLandIceMask = dsLandIceMask[['landIceMask']] dsLandIceMask = dsLandIceMask.isel(Time=0) self.landIceMask = dsLandIceMask.landIceMask > 0. @@ -555,12 +555,12 @@ def run_task(self): cellMasks = \ dsRegionMask.regionCellMasks.chunk({'nRegions': 10}) - restartFileName = \ - self.runStreams.readpath('restart')[0] + meshFilename = \ + self.runStreams.readpath('mesh')[0] - dsRestart = xr.open_dataset(restartFileName) - landIceFraction = dsRestart.landIceFraction.isel(Time=0) - areaCell = dsRestart.areaCell + dsMesh = xr.open_dataset(meshFilename) + landIceFraction = dsMesh.landIceFraction.isel(Time=0) + areaCell = dsMesh.areaCell # convert from kg/s to kg/yr totalMeltFlux = constants.sec_per_year * \ diff --git a/mpas_analysis/ocean/climatology_map_bsf.py b/mpas_analysis/ocean/climatology_map_bsf.py index 5cd36ba3a..44b62a283 100644 --- a/mpas_analysis/ocean/climatology_map_bsf.py +++ b/mpas_analysis/ocean/climatology_map_bsf.py @@ -315,7 +315,7 @@ def customize_masked_climatology(self, climatology, season): logger = self.logger config = self.config - ds_mesh = xr.open_dataset(self.restartFileName) + ds_mesh = xr.open_dataset(self.meshFilename) var_list = [ 'cellsOnEdge', 'cellsOnVertex', diff --git a/mpas_analysis/ocean/climatology_map_custom.py b/mpas_analysis/ocean/climatology_map_custom.py index 14a700875..6e934b851 100644 --- a/mpas_analysis/ocean/climatology_map_custom.py +++ b/mpas_analysis/ocean/climatology_map_custom.py @@ -316,10 +316,10 @@ def _add_thermal_forcing(self, climatology, derivedVars): press = dp.cumsum(dim='nVertLevels') - 0.5*dp # add land ice pressure if available - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) - if 'landIcePressure' in ds_restart: - press += ds_restart.landIcePressure + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) + if 'landIcePressure' in ds_mesh: + press += ds_mesh.landIcePressure tempFreeze = c0 + cs*salin + cp*press + cps*press*salin diff --git a/mpas_analysis/ocean/climatology_map_ohc_anomaly.py b/mpas_analysis/ocean/climatology_map_ohc_anomaly.py index ba5522d64..e2e7eb2cb 100644 --- a/mpas_analysis/ocean/climatology_map_ohc_anomaly.py +++ b/mpas_analysis/ocean/climatology_map_ohc_anomaly.py @@ -295,8 +295,8 @@ def _compute_ohc(self, climatology): Compute the OHC from the temperature and layer thicknesses in a given climatology data sets. """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) # specific heat [J/(kg*degC)] cp = self.namelist.getfloat('config_specific_heat_sea_water') @@ -305,10 +305,10 @@ def _compute_ohc(self, climatology): units_scale_factor = 1e-9 - n_vert_levels = ds_restart.sizes['nVertLevels'] + n_vert_levels = ds_mesh.sizes['nVertLevels'] - z_mid = compute_zmid(ds_restart.bottomDepth, ds_restart.maxLevelCell-1, - ds_restart.layerThickness) + z_mid = compute_zmid(ds_mesh.bottomDepth, ds_mesh.maxLevelCell-1, + ds_mesh.layerThickness) vert_index = xr.DataArray.from_dict( {'dims': ('nVertLevels',), 'data': np.arange(n_vert_levels)}) @@ -316,7 +316,7 @@ def _compute_ohc(self, climatology): temperature = climatology['timeMonthly_avg_activeTracers_temperature'] layer_thickness = climatology['timeMonthly_avg_layerThickness'] - masks = [vert_index < ds_restart.maxLevelCell, + masks = [vert_index < ds_mesh.maxLevelCell, z_mid <= self.min_depth, z_mid >= self.max_depth] for mask in masks: diff --git a/mpas_analysis/ocean/climatology_map_wind_stress_curl.py b/mpas_analysis/ocean/climatology_map_wind_stress_curl.py index d79df1ff6..4925084c8 100644 --- a/mpas_analysis/ocean/climatology_map_wind_stress_curl.py +++ b/mpas_analysis/ocean/climatology_map_wind_stress_curl.py @@ -174,7 +174,7 @@ def customize_masked_climatology(self, climatology, season): """ logger = self.logger - ds_mesh = xr.open_dataset(self.restartFileName) + ds_mesh = xr.open_dataset(self.meshFilename) var_list = [ 'verticesOnEdge', 'cellsOnVertex', diff --git a/mpas_analysis/ocean/compute_transects_subtask.py b/mpas_analysis/ocean/compute_transects_subtask.py index b2b7c8b45..c444130e4 100644 --- a/mpas_analysis/ocean/compute_transects_subtask.py +++ b/mpas_analysis/ocean/compute_transects_subtask.py @@ -247,7 +247,7 @@ def run_task(self): # first, get maxLevelCell and zMid, needed for masking - dsMesh = xr.open_dataset(self.restartFileName) + dsMesh = xr.open_dataset(self.meshFilename) dsMesh = dsMesh.isel(Time=0) self.maxLevelCell = dsMesh.maxLevelCell - 1 diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 92b2878aa..0ee9d15d3 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -247,10 +247,10 @@ def run_task(self): ds_mask = ds_region_mask.isel(nRegions=region_index) cell_mask = ds_mask.regionCellMasks == 1 - # Open the restart file, which contains unmasked weight variables - restart_filename = self.runStreams.readpath('restart')[0] - ds_restart = xarray.open_dataset(restart_filename) - ds_restart = ds_restart.isel(Time=0) + # Open the mesh file, which contains unmasked weight variables + mesh_filename = self.runStreams.readpath('mesh')[0] + ds_mesh = xarray.open_dataset(mesh_filename) + ds_mesh = ds_mesh.isel(Time=0) # Save the cell mask only for the region in its own file, which may be # referenced by future analysis (i.e., as a control run) @@ -263,13 +263,15 @@ def run_task(self): # Fetch the weight variables and mask them for each region for index, var in enumerate(self.variableList): weight_var_name = self.weightList[index] - if weight_var_name in ds_restart.keys(): + if weight_var_name in ds_mesh.keys(): var_name = f'timeMonthly_avg_{var}' ds_weights[f'{var_name}_weight'] = \ - ds_restart[weight_var_name].where(cell_mask, drop=True) + ds_mesh[weight_var_name].where(cell_mask, drop=True) else: - self.logger.warn(f'Weight variable {weight_var_name} is ' - f'not in the restart file, skipping') + self.logger.warning( + f'Weight variable {weight_var_name} is ' + f'not in the mesh file, skipping' + ) weights_filename = \ f'{base_directory}/{self.filePrefix}_{self.regionName}_weights.nc' diff --git a/mpas_analysis/ocean/meridional_heat_transport.py b/mpas_analysis/ocean/meridional_heat_transport.py index cfe2a69e6..f12448c6f 100644 --- a/mpas_analysis/ocean/meridional_heat_transport.py +++ b/mpas_analysis/ocean/meridional_heat_transport.py @@ -182,13 +182,15 @@ def run_task(self): # Read in depth and MHT latitude points # Latitude is from binBoundaryMerHeatTrans try: - restartFileName = self.runStreams.readpath('restart')[0] + meshFilename = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS-O restart file found: need at least ' - 'one for MHT calcuation') + raise IOError( + 'The MPAS-O mesh file was not found: neeeded for MHT ' + 'calculation' + ) - with xr.open_dataset(restartFileName) as dsRestart: - refBottomDepth = dsRestart.refBottomDepth + with xr.open_dataset(meshFilename) as dsMesh: + refBottomDepth = dsMesh.refBottomDepth nVertLevels = refBottomDepth.sizes['nVertLevels'] refLayerThickness = np.zeros(nVertLevels) diff --git a/mpas_analysis/ocean/ocean_regional_profiles.py b/mpas_analysis/ocean/ocean_regional_profiles.py index 470ae5cd2..0133d7878 100644 --- a/mpas_analysis/ocean/ocean_regional_profiles.py +++ b/mpas_analysis/ocean/ocean_regional_profiles.py @@ -344,22 +344,21 @@ def run_task(self): return # get areaCell - restartFileName = \ - self.runStreams.readpath('restart')[0] + meshFilename = self.runStreams.readpath('mesh')[0] - dsRestart = xr.open_dataset(restartFileName) - dsRestart = dsRestart.isel(Time=0) - areaCell = dsRestart.areaCell + dsMesh = xr.open_dataset(meshFilename) + dsMesh = dsMesh.isel(Time=0) + areaCell = dsMesh.areaCell - nVertLevels = dsRestart.sizes['nVertLevels'] + nVertLevels = dsMesh.sizes['nVertLevels'] vertIndex = \ xr.DataArray.from_dict({'dims': ('nVertLevels',), 'data': np.arange(nVertLevels)}) - vertMask = vertIndex < dsRestart.maxLevelCell + vertMask = vertIndex < dsMesh.maxLevelCell if self.max_bottom_depth is not None: - depthMask = dsRestart.bottomDepth < self.max_bottom_depth + depthMask = dsMesh.bottomDepth < self.max_bottom_depth vertDepthMask = np.logical_and(vertMask, depthMask) else: vertDepthMask = vertMask @@ -437,13 +436,15 @@ def run_task(self): # Note: restart file, not a mesh file because we need refBottomDepth, # not in a mesh file try: - restartFile = self.runStreams.readpath('restart')[0] + meshFilename = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS-O restart file found: need at least one ' - 'restart file for plotting time series vs. depth') + raise IOError( + 'MPAS-O mesh file not found: needed for plotting time series ' + 'vs. depth' + ) - with xr.open_dataset(restartFile) as dsRestart: - depths = dsRestart.refBottomDepth.values + with xr.open_dataset(meshFilename) as dsMesh: + depths = dsMesh.refBottomDepth.values z = np.zeros(depths.shape) z[0] = -0.5 * depths[0] z[1:] = -0.5 * (depths[0:-1] + depths[1:]) diff --git a/mpas_analysis/ocean/plot_hovmoller_subtask.py b/mpas_analysis/ocean/plot_hovmoller_subtask.py index b590263bb..36cf45a24 100644 --- a/mpas_analysis/ocean/plot_hovmoller_subtask.py +++ b/mpas_analysis/ocean/plot_hovmoller_subtask.py @@ -259,19 +259,21 @@ def run_task(self): ds = ds.set_xindex('regionNames') ds = ds.sel(regionNames=self.regionName) - # Note: restart file, not a mesh file because we need refBottomDepth, + # Note: mesh file, not a mesh file because we need refBottomDepth, # not in a mesh file try: - restartFile = self.runStreams.readpath('restart')[0] + meshFilename = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS-O restart file found: need at least one ' - 'restart file for plotting time series vs. depth') + raise IOError( + 'The MPAS-O mesh file was not found: needed for ' + 'plotting time series vs. depth' + ) # Define/read in general variables self.logger.info(' Read in depth...') - with xr.open_dataset(restartFile) as dsRestart: + with xr.open_dataset(meshFilename) as dsMesh: # reference depth [m] - depths = dsRestart.refBottomDepth.values + depths = dsMesh.refBottomDepth.values z = np.zeros(depths.shape) z[0] = -0.5 * depths[0] z[1:] = -0.5 * (depths[0:-1] + depths[1:]) diff --git a/mpas_analysis/ocean/regional_ts_diagrams.py b/mpas_analysis/ocean/regional_ts_diagrams.py index 10f516444..3bf41f124 100644 --- a/mpas_analysis/ocean/regional_ts_diagrams.py +++ b/mpas_analysis/ocean/regional_ts_diagrams.py @@ -601,17 +601,17 @@ def _write_mpas_t_s(self, config): chunk = {'nCells': cellsChunk} try: - restartFileName = self.runStreams.readpath('restart')[0] + meshFilename = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS-O restart file found: need at least one' - ' restart file to plot T-S diagrams') - dsRestart = xarray.open_dataset(restartFileName) - dsRestart = dsRestart.isel(Time=0) - if 'landIceMask' in dsRestart: - landIceMask = dsRestart.landIceMask + raise IOError('No MPAS-O mesh file found: need at least one' + ' mesh file to plot T-S diagrams') + dsMesh = xarray.open_dataset(meshFilename) + dsMesh = dsMesh.isel(Time=0) + if 'landIceMask' in dsMesh: + landIceMask = dsMesh.landIceMask else: landIceMask = None - dsRestart = dsRestart.chunk(chunk) + dsMesh = dsMesh.chunk(chunk) regionMaskFileName = self.mpasMasksSubtask.maskFileName @@ -653,11 +653,11 @@ def _write_mpas_t_s(self, config): 'timeMonthly_avg_layerThickness'] ds = ds[variableList] - ds['zMid'] = compute_zmid(dsRestart.bottomDepth, - dsRestart.maxLevelCell-1, - dsRestart.layerThickness) + ds['zMid'] = compute_zmid(dsMesh.bottomDepth, + dsMesh.maxLevelCell-1, + dsMesh.layerThickness) - ds['volume'] = (dsRestart.areaCell * + ds['volume'] = (dsMesh.areaCell * ds['timeMonthly_avg_layerThickness']) ds.load() diff --git a/mpas_analysis/ocean/remap_depth_slices_subtask.py b/mpas_analysis/ocean/remap_depth_slices_subtask.py index 4d9cbd03c..7c60c89e4 100644 --- a/mpas_analysis/ocean/remap_depth_slices_subtask.py +++ b/mpas_analysis/ocean/remap_depth_slices_subtask.py @@ -111,8 +111,8 @@ def run_task(self): # ------- # Xylar Asay-Davis - # first, load the land-ice mask from the restart file - ds = xr.open_dataset(self.restartFileName) + # first, load the land-ice mask from the mesh file + ds = xr.open_dataset(self.meshFilename) ds = ds[['maxLevelCell', 'bottomDepth', 'layerThickness']] ds = ds.isel(Time=0) diff --git a/mpas_analysis/ocean/streamfunction_moc.py b/mpas_analysis/ocean/streamfunction_moc.py index ca69a8918..758ce988e 100644 --- a/mpas_analysis/ocean/streamfunction_moc.py +++ b/mpas_analysis/ocean/streamfunction_moc.py @@ -363,13 +363,15 @@ def _compute_moc_climo_analysismember(self): # Read in depth and bin latitudes try: - restartFileName = self.runStreams.readpath('restart')[0] + meshFilename = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS-O restart file found: need at least ' - 'one for MHT calcuation') + raise IOError( + 'The MPAS-O mesh file could not be found: needed for MOC ' + 'calculation' + ) - with xr.open_dataset(restartFileName) as dsRestart: - refBottomDepth = dsRestart.refBottomDepth.values + with xr.open_dataset(meshFilename) as dsMesh: + refBottomDepth = dsMesh.refBottomDepth.values nVertLevels = len(refBottomDepth) refLayerThickness = np.zeros(nVertLevels) @@ -1082,13 +1084,17 @@ def _compute_moc_time_series_analysismember(self): sizes = dsLocal.sizes moc = np.zeros((len(inputFiles), sizes['nVertLevels']+1, len(binBoundaryMocStreamfunction))) + try: - restartFile = self.runStreams.readpath('restart')[0] + meshFilename = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS-O restart file found: need at ' - 'least one restart file for MOC calculation') - with xr.open_dataset(restartFile) as dsRestart: - refBottomDepth = dsRestart.refBottomDepth.values + raise IOError( + 'The MPAS-O mesh file could not be found: needed for ' + 'MOC calculation' + ) + + with xr.open_dataset(meshFilename) as dsMesh: + refBottomDepth = dsMesh.refBottomDepth.values nVertLevels = len(refBottomDepth) refTopDepth = np.zeros(nVertLevels + 1) refTopDepth[1:nVertLevels + 1] = refBottomDepth[0:nVertLevels] @@ -1572,12 +1578,16 @@ def _load_moc(self, config): def _load_mesh(runStreams): # Load mesh related variables + try: - restartFile = runStreams.readpath('restart')[0] + meshFilename = runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS-O restart file found: need at least one ' - 'restart file for MOC calculation') - ncFile = netCDF4.Dataset(restartFile, mode='r') + raise IOError( + 'The MPAS-O mesh file could not be found: needed for ' + 'MOC calculation' + ) + + ncFile = netCDF4.Dataset(meshFilename, mode='r') dvEdge = ncFile.variables['dvEdge'][:] areaCell = ncFile.variables['areaCell'][:] refBottomDepth = ncFile.variables['refBottomDepth'][:] diff --git a/mpas_analysis/ocean/time_series_antarctic_melt.py b/mpas_analysis/ocean/time_series_antarctic_melt.py index 911db61b6..1cbc06a0b 100644 --- a/mpas_analysis/ocean/time_series_antarctic_melt.py +++ b/mpas_analysis/ocean/time_series_antarctic_melt.py @@ -175,7 +175,6 @@ def __init__(self, parentTask, startYear, endYear, mpasTimeSeriesTask, self.run_after(masksSubtask) self.iceShelvesToPlot = iceShelvesToPlot - self.restartFileName = None self.startYear = startYear self.endYear = endYear self.startDate = f'{self.startYear:04d}-01-01_00:00:00' @@ -217,13 +216,6 @@ def setup_and_check(self): ' Otherwise, no melt rates are available \n' ' for plotting.') - # Load mesh related variables - try: - self.restartFileName = self.runStreams.readpath('restart')[0] - except ValueError: - raise IOError('No MPAS-O restart file found: need at least one ' - 'restart file for Antarctic melt calculations') - totalFluxVar = 'timeMonthly_avg_landIceFreshwaterFluxTotal' landIceFluxVar = 'timeMonthly_avg_landIceFreshwaterFlux' if totalFluxVar in self.mpasTimeSeriesTask.allVariables: @@ -284,12 +276,12 @@ def run_task(self): f'Deleting it.') os.remove(outFileName) - restartFileName = \ - mpasTimeSeriesTask.runStreams.readpath('restart')[0] + meshFilename = \ + mpasTimeSeriesTask.runStreams.readpath('mesh')[0] - dsRestart = xarray.open_dataset(restartFileName) - landIceFraction = dsRestart.landIceFraction.isel(Time=0) - areaCell = dsRestart.areaCell + dsMesh = xarray.open_dataset(meshFilename) + landIceFraction = dsMesh.landIceFraction.isel(Time=0) + areaCell = dsMesh.areaCell regionMaskFileName = self.masksSubtask.maskFileName diff --git a/mpas_analysis/ocean/time_series_ocean_regions.py b/mpas_analysis/ocean/time_series_ocean_regions.py index b235f825f..a56069a79 100644 --- a/mpas_analysis/ocean/time_series_ocean_regions.py +++ b/mpas_analysis/ocean/time_series_ocean_regions.py @@ -296,10 +296,12 @@ def run_task(self): # Load mesh related variables try: - restartFileName = self.runStreams.readpath('restart')[0] + meshFilename = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS-O restart file found: need at least one ' - 'restart file for ocean region time series') + raise IOError( + 'The MPAS-O mesh file could not be found: needed for ocean ' + 'region time series' + ) if config.has_option(sectionName, 'zmin'): config_zmin = config.getfloat(sectionName, 'zmin') @@ -311,13 +313,13 @@ def run_task(self): else: config_zmax = None - dsRestart = xarray.open_dataset(restartFileName).isel(Time=0) - zMid = compute_zmid(dsRestart.bottomDepth, dsRestart.maxLevelCell-1, - dsRestart.layerThickness) - areaCell = dsRestart.areaCell - if 'landIceMask' in dsRestart: + dsMesh = xarray.open_dataset(meshFilename).isel(Time=0) + zMid = compute_zmid(dsMesh.bottomDepth, dsMesh.maxLevelCell-1, + dsMesh.layerThickness) + areaCell = dsMesh.areaCell + if 'landIceMask' in dsMesh: # only the region outside of ice-shelf cavities - openOceanMask = dsRestart.landIceMask == 0 + openOceanMask = dsMesh.landIceMask == 0 else: openOceanMask = None diff --git a/mpas_analysis/ocean/time_series_ohc_anomaly.py b/mpas_analysis/ocean/time_series_ohc_anomaly.py index 7a8de35f8..a4132f808 100644 --- a/mpas_analysis/ocean/time_series_ohc_anomaly.py +++ b/mpas_analysis/ocean/time_series_ohc_anomaly.py @@ -152,20 +152,21 @@ def _compute_ohc(self, ds): ds.ohc.attrs['units'] = '$10^{22}$ J' ds.ohc.attrs['description'] = 'Ocean heat content in each region' - # Note: restart file, not a mesh file because we need refBottomDepth, - # not in a mesh file try: - restartFile = self.runStreams.readpath('restart')[0] + meshFile = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS-O restart file found: need at least one ' - 'restart file for OHC calculation') + raise IOError( + 'The MPAS-O mesh file could not be found: needed for OHC ' + 'calculation' + ) # Define/read in general variables - with xr.open_dataset(restartFile) as dsRestart: + with xr.open_dataset(meshFile) as dsMesh: # reference depth [m] # add depths as a coordinate to the data set - ds.coords['depth'] = (('nVertLevels',), - dsRestart.refBottomDepth.values) + ds.coords['depth'] = ( + ('nVertLevels',), dsMesh.refBottomDepth.values + ) return ds diff --git a/mpas_analysis/ocean/time_series_transport.py b/mpas_analysis/ocean/time_series_transport.py index 0d00a6a64..fe461fed6 100644 --- a/mpas_analysis/ocean/time_series_transport.py +++ b/mpas_analysis/ocean/time_series_transport.py @@ -124,7 +124,7 @@ class ComputeTransportSubtask(AnalysisTask): transectsToPlot : list of str A list of transects to plot - + groupSuffix : str standard transects vs Arctic transects """ @@ -159,7 +159,7 @@ def __init__(self, parentTask, startYear, endYear, # Authors # ------- # Xylar Asay-Davis - subtaskName = f'compute{groupSuffix}_{startYear:04d}-{endYear:04d}' + subtaskName = f'compute{groupSuffix}_{startYear:04d}-{endYear:04d}' # first, call the constructor from the base class (AnalysisTask) super().__init__( config=parentTask.config, @@ -167,7 +167,7 @@ def __init__(self, parentTask, startYear, endYear, componentName=parentTask.componentName, tags=parentTask.tags, subtaskName=subtaskName) - + self.subprocessCount = self.config.getint(f'timeSeries{groupSuffix}', 'subprocessCount') self.startYear = startYear @@ -177,7 +177,7 @@ def __init__(self, parentTask, startYear, endYear, self.run_after(masksSubtask) self.transectsToPlot = transectsToPlot - self.restartFileName = None + self.meshFilename = None self.groupSuffix = groupSuffix def setup_and_check(self): @@ -209,10 +209,12 @@ def setup_and_check(self): # Load mesh related variables try: - self.restartFileName = self.runStreams.readpath('restart')[0] + self.meshFilename = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS-O restart file found: need at least one ' - 'restart file for transport calculations') + raise IOError( + 'The MPAS-O mesh file was not found: needed for transport ' + 'calculations' + ) def run_task(self): """ @@ -293,7 +295,7 @@ def run_task(self): # figure out the indices of the transects to plot maskTransectNames = decode_strings(dsTransectMask.transectNames) - dsMesh = xarray.open_dataset(self.restartFileName) + dsMesh = xarray.open_dataset(self.meshFilename) dsMesh = dsMesh[['dvEdge', 'cellsOnEdge']] dsMesh.load() dvEdge = dsMesh.dvEdge @@ -417,7 +419,7 @@ def __init__(self, parentTask, startYears, endYears, groupSuffix): # Authors # ------- # Xylar Asay-Davis - + # first, call the constructor from the base class (AnalysisTask) super(CombineTransportSubtask, self).__init__( config=parentTask.config, @@ -474,7 +476,7 @@ class PlotTransportSubtask(AnalysisTask): transportGroup : str (with spaces) standard transects (``Transport Transects``) vs Arctic transects (``Arctic Transport Transects``) - + """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_albedo.py b/mpas_analysis/sea_ice/climatology_map_albedo.py index 685838377..35c00e59e 100755 --- a/mpas_analysis/sea_ice/climatology_map_albedo.py +++ b/mpas_analysis/sea_ice/climatology_map_albedo.py @@ -250,8 +250,8 @@ def _compute_albedo(self, climatology): """ Compute the albedo """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) albedo = climatology['timeMonthly_avg_broadbandAlbedo'] diff --git a/mpas_analysis/sea_ice/climatology_map_area_pond.py b/mpas_analysis/sea_ice/climatology_map_area_pond.py index 516c28d17..595bb3a6c 100755 --- a/mpas_analysis/sea_ice/climatology_map_area_pond.py +++ b/mpas_analysis/sea_ice/climatology_map_area_pond.py @@ -250,8 +250,8 @@ def _compute_pondarea(self, climatology): """ Compute the melt pond area fraction """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) pondarea = climatology['timeMonthly_avg_meltPondAreaFinalArea'] diff --git a/mpas_analysis/sea_ice/climatology_map_area_ridge.py b/mpas_analysis/sea_ice/climatology_map_area_ridge.py index 953a4863b..11c79c138 100755 --- a/mpas_analysis/sea_ice/climatology_map_area_ridge.py +++ b/mpas_analysis/sea_ice/climatology_map_area_ridge.py @@ -252,14 +252,13 @@ def customize_masked_climatology(self, climatology, season): def _compute_ridgefraction(self, climatology): """ - Compute the mean ridge thickness in m + Compute the mean ridge thickness in m """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) ridgearea = climatology['timeMonthly_avg_ridgedIceAreaAverage'] - area = climatology['timeMonthly_avg_iceAreaCell'] ridgefraction = ridgearea # area fraction of sea ice -# ridgefraction = ridgearea*area # area fraction of grid cell + return ridgefraction diff --git a/mpas_analysis/sea_ice/climatology_map_melting.py b/mpas_analysis/sea_ice/climatology_map_melting.py index 0bd1efde7..129249e7a 100755 --- a/mpas_analysis/sea_ice/climatology_map_melting.py +++ b/mpas_analysis/sea_ice/climatology_map_melting.py @@ -254,8 +254,8 @@ def _compute_melting(self, climatology): Compute the total sea ice melting in m yr^-1 from the individual melt fields in m s^-1. """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) units_scale_factor = 60 * 60 * 24 * 365 @@ -266,6 +266,7 @@ def _compute_melting(self, climatology): melting = (basal + surface + lateral) * units_scale_factor return melting + class RemapAnIceFluxMeltingClimatology(RemapObservedClimatologySubtask): """ A subtask for reading and remapping sea ice melting from AnIceFlux diff --git a/mpas_analysis/sea_ice/climatology_map_production.py b/mpas_analysis/sea_ice/climatology_map_production.py index 4f69e49e4..043332437 100755 --- a/mpas_analysis/sea_ice/climatology_map_production.py +++ b/mpas_analysis/sea_ice/climatology_map_production.py @@ -254,8 +254,8 @@ def _compute_production(self, climatology): Compute the total sea ice production in m yr^-1 from the individual production fields in m s^-1. """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) units_scale_factor = 60 * 60 * 24 * 365 @@ -266,6 +266,7 @@ def _compute_production(self, climatology): production = (congelation + frazil + snowice) * units_scale_factor return production + class RemapAnIceFluxProductionClimatology(RemapObservedClimatologySubtask): """ A subtask for reading and remapping sea ice production from AnIceFlux diff --git a/mpas_analysis/sea_ice/climatology_map_snow_depth.py b/mpas_analysis/sea_ice/climatology_map_snow_depth.py index 9511b7700..a8bd7c732 100755 --- a/mpas_analysis/sea_ice/climatology_map_snow_depth.py +++ b/mpas_analysis/sea_ice/climatology_map_snow_depth.py @@ -250,14 +250,12 @@ def customize_masked_climatology(self, climatology, season): def _compute_snowdepth(self, climatology): """ - Compute the snow depth in m + Compute the snow depth in m """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) volume = climatology['timeMonthly_avg_snowVolumeCell'] - area = climatology['timeMonthly_avg_iceAreaCell'] snowdepth = volume # volume per unit grid cell area (m) -# snowdepth = volume/area # volume per unit sea ice area (m) return snowdepth diff --git a/mpas_analysis/sea_ice/climatology_map_snowice.py b/mpas_analysis/sea_ice/climatology_map_snowice.py index 595401138..08c0e833f 100755 --- a/mpas_analysis/sea_ice/climatology_map_snowice.py +++ b/mpas_analysis/sea_ice/climatology_map_snowice.py @@ -255,8 +255,8 @@ def _compute_snowice(self, climatology): """ Compute the snow-ice formation rate in m yr^-1 """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) units_scale_factor = 60 * 60 * 24 * 365 diff --git a/mpas_analysis/sea_ice/climatology_map_snowmelt.py b/mpas_analysis/sea_ice/climatology_map_snowmelt.py index 74bd49724..0aefb3008 100755 --- a/mpas_analysis/sea_ice/climatology_map_snowmelt.py +++ b/mpas_analysis/sea_ice/climatology_map_snowmelt.py @@ -250,8 +250,8 @@ def _compute_snowmelt(self, climatology): """ Compute the snow melt rate in m yr^-1 """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) units_scale_factor = 60 * 60 * 24 * 365 diff --git a/mpas_analysis/sea_ice/climatology_map_tendency_area_thermo.py b/mpas_analysis/sea_ice/climatology_map_tendency_area_thermo.py index 0942b3632..968767e3e 100755 --- a/mpas_analysis/sea_ice/climatology_map_tendency_area_thermo.py +++ b/mpas_analysis/sea_ice/climatology_map_tendency_area_thermo.py @@ -251,10 +251,13 @@ def _compute_tendency(self, climatology): """ Compute the tendency in area fraction/yr """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) units_scale_factor = 60 * 60 * 24 * 365 - tendency = climatology['timeMonthly_avg_iceAreaTendencyThermodynamics'] * units_scale_factor + tendency = ( + climatology['timeMonthly_avg_iceAreaTendencyThermodynamics'] * + units_scale_factor + ) return tendency diff --git a/mpas_analysis/sea_ice/climatology_map_tendency_area_transp.py b/mpas_analysis/sea_ice/climatology_map_tendency_area_transp.py index 4986163e1..e032f0dc4 100755 --- a/mpas_analysis/sea_ice/climatology_map_tendency_area_transp.py +++ b/mpas_analysis/sea_ice/climatology_map_tendency_area_transp.py @@ -251,10 +251,13 @@ def _compute_tendency(self, climatology): """ Compute the tendency in fraction/yr """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) units_scale_factor = 60 * 60 * 24 * 365 - tendency = climatology['timeMonthly_avg_iceAreaTendencyTransport'] * units_scale_factor + tendency = ( + climatology['timeMonthly_avg_iceAreaTendencyTransport'] * + units_scale_factor + ) return tendency diff --git a/mpas_analysis/sea_ice/climatology_map_tendency_volume_thermo.py b/mpas_analysis/sea_ice/climatology_map_tendency_volume_thermo.py index 2045cbf74..7994604f1 100755 --- a/mpas_analysis/sea_ice/climatology_map_tendency_volume_thermo.py +++ b/mpas_analysis/sea_ice/climatology_map_tendency_volume_thermo.py @@ -252,10 +252,13 @@ def _compute_tendency(self, climatology): """ Compute the tendency in m/yr """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) units_scale_factor = 60 * 60 * 24 * 365 - tendency = climatology['timeMonthly_avg_iceVolumeTendencyThermodynamics'] * units_scale_factor + tendency = ( + climatology['timeMonthly_avg_iceVolumeTendencyThermodynamics'] * + units_scale_factor + ) return tendency diff --git a/mpas_analysis/sea_ice/climatology_map_tendency_volume_transp.py b/mpas_analysis/sea_ice/climatology_map_tendency_volume_transp.py index f91fa7aa9..5b4e17a7f 100755 --- a/mpas_analysis/sea_ice/climatology_map_tendency_volume_transp.py +++ b/mpas_analysis/sea_ice/climatology_map_tendency_volume_transp.py @@ -251,10 +251,13 @@ def _compute_tendency(self, climatology): """ Compute the tendency in m/yr """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) units_scale_factor = 60 * 60 * 24 * 365 - tendency = climatology['timeMonthly_avg_iceVolumeTendencyTransport'] * units_scale_factor + tendency = ( + climatology['timeMonthly_avg_iceVolumeTendencyTransport'] * + units_scale_factor + ) return tendency diff --git a/mpas_analysis/sea_ice/climatology_map_volume_ridge.py b/mpas_analysis/sea_ice/climatology_map_volume_ridge.py index 987edbd91..14c98ac03 100755 --- a/mpas_analysis/sea_ice/climatology_map_volume_ridge.py +++ b/mpas_analysis/sea_ice/climatology_map_volume_ridge.py @@ -251,14 +251,12 @@ def customize_masked_climatology(self, climatology, season): def _compute_volumeridge(self, climatology): """ - Compute the mean ridge thickness in m + Compute the mean ridge thickness in m """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) volume = climatology['timeMonthly_avg_ridgedIceVolumeAverage'] - area = climatology['timeMonthly_avg_iceAreaCell'] volumeridge = volume # volume per unit sea ice area (m) -# volumeridge = volume*area # volume per unit grid cell area (m) return volumeridge diff --git a/mpas_analysis/sea_ice/time_series.py b/mpas_analysis/sea_ice/time_series.py index a3a77c23a..12726bc56 100644 --- a/mpas_analysis/sea_ice/time_series.py +++ b/mpas_analysis/sea_ice/time_series.py @@ -136,11 +136,12 @@ def setup_and_check(self): self.simulationStartTime = get_simulation_start_time(self.runStreams) try: - self.restartFileName = self.runStreams.readpath('restart')[0] + self.meshFilename = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS-SeaIce restart file found: need at least ' - 'one restart file to perform remapping of ' - 'climatologies.') + raise IOError( + 'The MPAS-seaice mesh file could not be found: needed to ' + 'compute sea ice time series.' + ) # these are redundant for now. Later cleanup is needed where these # file names are reused in run() @@ -667,7 +668,7 @@ def _compute_area_vol(self): outFileNames[hemisphere] = outFileName dsTimeSeries = {} - dsMesh = xr.open_dataset(self.restartFileName) + dsMesh = xr.open_dataset(self.meshFilename) dsMesh = dsMesh[['latCell', 'areaCell']] # Load data ds = open_mpas_dataset( diff --git a/mpas_analysis/shared/climatology/remap_mpas_climatology_subtask.py b/mpas_analysis/shared/climatology/remap_mpas_climatology_subtask.py index 58fd2e394..2661992e1 100644 --- a/mpas_analysis/shared/climatology/remap_mpas_climatology_subtask.py +++ b/mpas_analysis/shared/climatology/remap_mpas_climatology_subtask.py @@ -62,8 +62,8 @@ class RemapMpasClimatologySubtask(AnalysisTask): Descriptors of the comparison grids to use for remapping, with grid names as the keys. - restartFileName : str - If ``comparisonGridName`` is not ``None``, the name of a restart + meshFilename : str + If ``comparisonGridName`` is not ``None``, the name of the mesh file from which the MPAS mesh can be read. useNcremap : bool, optional @@ -182,6 +182,7 @@ def __init__(self, mpasClimatologyTask, parentTask, climatologyName, self.useNcremap = useNcremap self.vertices = vertices + self.meshFilename = None def setup_and_check(self): """ @@ -206,11 +207,11 @@ def setup_and_check(self): super(RemapMpasClimatologySubtask, self).setup_and_check() try: - self.restartFileName = self.runStreams.readpath('restart')[0] + self.meshFilename = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS restart file found: need at least one ' - 'restart file to perform remapping of ' - 'climatologies.') + raise IOError( + 'The MPAS mesh file could not be found: needed to perform ' + 'remapping of climatologies.') # we set up the remapper here because ESFM_RegridWeightGen seems to # have trouble if it runs in another process (or in several at once) @@ -419,10 +420,10 @@ def _setup_remappers(self): meshName = config.get('input', 'mpasMeshName') if self.vertices: mpasDescriptor = MpasVertexMeshDescriptor( - self.restartFileName, mesh_name=meshName) + self.meshFilename, mesh_name=meshName) else: mpasDescriptor = MpasCellMeshDescriptor( - self.restartFileName, mesh_name=meshName) + self.meshFilename, mesh_name=meshName) self.mpasMeshName = mpasDescriptor.mesh_name self.remappers[comparisonGridName] = get_remapper( diff --git a/mpas_analysis/shared/regions/compute_region_masks_subtask.py b/mpas_analysis/shared/regions/compute_region_masks_subtask.py index b72c1fb45..ebaccc8ec 100644 --- a/mpas_analysis/shared/regions/compute_region_masks_subtask.py +++ b/mpas_analysis/shared/regions/compute_region_masks_subtask.py @@ -119,7 +119,7 @@ class ComputeRegionMasksSubtask(AnalysisTask): The name of the output mask file obsFileName : str - The name of an observations file to create masks for. But default, + The name of an observations file to create masks for. By default, lon/lat are taken from an MPAS restart file lonVar, latVar : str @@ -269,10 +269,12 @@ def setup_and_check(self): if self.useMpasMesh: try: - self.obsFileName = self.runStreams.readpath('restart')[0] + self.obsFileName = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS restart file found: need at least one ' - 'restart file to perform region masking.') + raise IOError( + 'The MPAS mesh file could not be found: needed to perform ' + 'region masking.' + ) maskSubdirectory = build_config_full_path(self.config, 'output', 'maskSubdirectory') diff --git a/mpas_analysis/shared/transects/compute_transect_masks_subtask.py b/mpas_analysis/shared/transects/compute_transect_masks_subtask.py index 44d75e7ec..1cc6d4d75 100644 --- a/mpas_analysis/shared/transects/compute_transect_masks_subtask.py +++ b/mpas_analysis/shared/transects/compute_transect_masks_subtask.py @@ -194,10 +194,12 @@ def setup_and_check(self): super(ComputeTransectMasksSubtask, self).setup_and_check() try: - self.obsFileName = self.runStreams.readpath('restart')[0] + self.obsFileName = self.runStreams.readpath('mesh')[0] except ValueError: - raise IOError('No MPAS restart file found: need at least one ' - 'restart file to perform region masking.') + raise IOError( + 'The MPAS mesh file could not be found: needed to perform ' + 'transect masking.' + ) self.maskSubdirectory = build_config_full_path(self.config, 'output', 'maskSubdirectory') From b91b631d3296bc26d71be416fb44c53d698d4bee Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 8 Aug 2025 11:16:36 +0200 Subject: [PATCH 076/116] Update tutorials to use mesh, not restart, files --- docs/tutorials/dev_add_task.rst | 4 ++-- docs/tutorials/dev_understand_a_task.rst | 26 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/tutorials/dev_add_task.rst b/docs/tutorials/dev_add_task.rst index fc84e82a6..69539a078 100644 --- a/docs/tutorials/dev_add_task.rst +++ b/docs/tutorials/dev_add_task.rst @@ -997,7 +997,7 @@ script into the ``customize_masked_climatology()`` function: """ logger = self.logger - ds_mesh = xr.open_dataset(self.restartFileName) + ds_mesh = xr.open_dataset(self.meshFilename) ds_mesh = ds_mesh[['cellsOnEdge', 'cellsOnVertex', 'nEdgesOnCell', 'edgesOnCell', 'verticesOnCell', 'verticesOnEdge', 'dcEdge', 'dvEdge']] @@ -1313,7 +1313,7 @@ described in this tutorial: """ logger = self.logger - ds_mesh = xr.open_dataset(self.restartFileName) + ds_mesh = xr.open_dataset(self.meshFilename) ds_mesh = ds_mesh[['cellsOnEdge', 'cellsOnVertex', 'nEdgesOnCell', 'edgesOnCell', 'verticesOnCell', 'verticesOnEdge', 'dcEdge', 'dvEdge']] diff --git a/docs/tutorials/dev_understand_a_task.rst b/docs/tutorials/dev_understand_a_task.rst index b27607c04..a02694f7f 100644 --- a/docs/tutorials/dev_understand_a_task.rst +++ b/docs/tutorials/dev_understand_a_task.rst @@ -779,8 +779,8 @@ at that before we continue with ``customize_masked_climatology()``. Compute the OHC from the temperature and layer thicknesses in a given climatology data sets. """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) # specific heat [J/(kg*degC)] cp = self.namelist.getfloat('config_specific_heat_sea_water') @@ -789,10 +789,10 @@ at that before we continue with ``customize_masked_climatology()``. units_scale_factor = 1e-9 - n_vert_levels = ds_restart.sizes['nVertLevels'] + n_vert_levels = ds_mesh.sizes['nVertLevels'] - z_mid = compute_zmid(ds_restart.bottomDepth, ds_restart.maxLevelCell-1, - ds_restart.layerThickness) + z_mid = compute_zmid(ds_mesh.bottomDepth, ds_mesh.maxLevelCell-1, + ds_mesh.layerThickness) vert_index = xr.DataArray.from_dict( {'dims': ('nVertLevels',), 'data': np.arange(n_vert_levels)}) @@ -800,7 +800,7 @@ at that before we continue with ``customize_masked_climatology()``. temperature = climatology['timeMonthly_avg_activeTracers_temperature'] layer_thickness = climatology['timeMonthly_avg_layerThickness'] - masks = [vert_index < ds_restart.maxLevelCell, + masks = [vert_index < ds_mesh.maxLevelCell, z_mid <= self.min_depth, z_mid >= self.max_depth] for mask in masks: @@ -812,7 +812,7 @@ at that before we continue with ``customize_masked_climatology()``. return ohc This function uses a combination of mesh information taken from an MPAS -restart file (available from the ``self.restartFileName`` attribute inherited +mesh file (available from the ``self.meshFilename`` attribute inherited from :py:class:`~mpas_analysis.shared.climatology.RemapMpasClimatologySubtask`), namelist options available from the ``self.namelist`` reader (inherited from :py:class:`~mpas_analysis.shared.AnalysisTask`), and ``temperature`` and @@ -1160,8 +1160,8 @@ here is the full analysis task as described in this tutorial: Compute the OHC from the temperature and layer thicknesses in a given climatology data sets. """ - ds_restart = xr.open_dataset(self.restartFileName) - ds_restart = ds_restart.isel(Time=0) + ds_mesh = xr.open_dataset(self.meshFilename) + ds_mesh = ds_mesh.isel(Time=0) # specific heat [J/(kg*degC)] cp = self.namelist.getfloat('config_specific_heat_sea_water') @@ -1170,10 +1170,10 @@ here is the full analysis task as described in this tutorial: units_scale_factor = 1e-9 - n_vert_levels = ds_restart.sizes['nVertLevels'] + n_vert_levels = ds_mesh.sizes['nVertLevels'] - z_mid = compute_zmid(ds_restart.bottomDepth, ds_restart.maxLevelCell-1, - ds_restart.layerThickness) + z_mid = compute_zmid(ds_mesh.bottomDepth, ds_mesh.maxLevelCell-1, + ds_mesh.layerThickness) vert_index = xr.DataArray.from_dict( {'dims': ('nVertLevels',), 'data': np.arange(n_vert_levels)}) @@ -1181,7 +1181,7 @@ here is the full analysis task as described in this tutorial: temperature = climatology['timeMonthly_avg_activeTracers_temperature'] layer_thickness = climatology['timeMonthly_avg_layerThickness'] - masks = [vert_index < ds_restart.maxLevelCell, + masks = [vert_index < ds_mesh.maxLevelCell, z_mid <= self.min_depth, z_mid >= self.max_depth] for mask in masks: From 61236de1a0dec09cda93f414b051255d85f20427 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 8 Aug 2025 11:46:23 +0200 Subject: [PATCH 077/116] Fix tests --- mpas_analysis/test/test_mpas_climatology_task/streams.ocean | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mpas_analysis/test/test_mpas_climatology_task/streams.ocean b/mpas_analysis/test/test_mpas_climatology_task/streams.ocean index 1a025681b..392f0b731 100644 --- a/mpas_analysis/test/test_mpas_climatology_task/streams.ocean +++ b/mpas_analysis/test/test_mpas_climatology_task/streams.ocean @@ -3,7 +3,7 @@ Date: Mon, 11 Aug 2025 05:50:32 -0500 Subject: [PATCH 078/116] Add get_mesh_filename() method and use it in all tasks This enables us to use different streams for ocean and sea-ice tasks, since the only stream where the mesh is available in MPAS-seaice is the landIceMasks stream. --- mpas_analysis/default.cfg | 15 +++++++++ .../ocean/climatology_map_antarctic_melt.py | 3 +- mpas_analysis/ocean/histogram.py | 2 +- .../ocean/meridional_heat_transport.py | 8 +---- .../ocean/ocean_regional_profiles.py | 10 ++---- mpas_analysis/ocean/plot_hovmoller_subtask.py | 8 +---- mpas_analysis/ocean/regional_ts_diagrams.py | 6 +--- mpas_analysis/ocean/streamfunction_moc.py | 31 +++---------------- .../ocean/time_series_antarctic_melt.py | 3 +- .../ocean/time_series_ocean_regions.py | 8 +---- .../ocean/time_series_ohc_anomaly.py | 8 +---- mpas_analysis/ocean/time_series_transport.py | 8 +---- mpas_analysis/sea_ice/time_series.py | 8 +---- mpas_analysis/shared/analysis_task.py | 21 +++++++++++++ .../remap_mpas_climatology_subtask.py | 7 +---- .../regions/compute_region_masks_subtask.py | 8 +---- .../compute_transect_masks_subtask.py | 8 +---- 17 files changed, 56 insertions(+), 106 deletions(-) diff --git a/mpas_analysis/default.cfg b/mpas_analysis/default.cfg index 4596f72f8..00cb1882d 100755 --- a/mpas_analysis/default.cfg +++ b/mpas_analysis/default.cfg @@ -527,6 +527,13 @@ lonLines = np.arange(-180., 181., 30.) generate = True +[ocean] +## options related to ocean analysis + +# the name of the stream that points to the MPAS mesh file. +meshStream = mesh + + [oceanObservations] ## options related to ocean observations with which the results will be ## compared @@ -585,6 +592,14 @@ remappedClimSubdirectory = clim/obs/remapped baseDirectory = /dir/to/ocean/reference +[seaIce] +## options related to sea-ice analysis + +# the name of the stream that points to the MPAS mesh file. The "mesh" stream +# in MPAS-seaice points to a dummy file, so it is not useful for this purpose. +meshStream = landIceMasks + + [seaIceObservations] ## options related to sea ice observations with which the results will be ## compared diff --git a/mpas_analysis/ocean/climatology_map_antarctic_melt.py b/mpas_analysis/ocean/climatology_map_antarctic_melt.py index 60fa7a20b..122e9d3a5 100644 --- a/mpas_analysis/ocean/climatology_map_antarctic_melt.py +++ b/mpas_analysis/ocean/climatology_map_antarctic_melt.py @@ -555,8 +555,7 @@ def run_task(self): cellMasks = \ dsRegionMask.regionCellMasks.chunk({'nRegions': 10}) - meshFilename = \ - self.runStreams.readpath('mesh')[0] + meshFilename = self.get_mesh_filename() dsMesh = xr.open_dataset(meshFilename) landIceFraction = dsMesh.landIceFraction.isel(Time=0) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 0ee9d15d3..3f99a2031 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -248,7 +248,7 @@ def run_task(self): cell_mask = ds_mask.regionCellMasks == 1 # Open the mesh file, which contains unmasked weight variables - mesh_filename = self.runStreams.readpath('mesh')[0] + mesh_filename = self.get_mesh_filename() ds_mesh = xarray.open_dataset(mesh_filename) ds_mesh = ds_mesh.isel(Time=0) diff --git a/mpas_analysis/ocean/meridional_heat_transport.py b/mpas_analysis/ocean/meridional_heat_transport.py index f12448c6f..10f3b5590 100644 --- a/mpas_analysis/ocean/meridional_heat_transport.py +++ b/mpas_analysis/ocean/meridional_heat_transport.py @@ -181,13 +181,7 @@ def run_task(self): # Read in depth and MHT latitude points # Latitude is from binBoundaryMerHeatTrans - try: - meshFilename = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'The MPAS-O mesh file was not found: neeeded for MHT ' - 'calculation' - ) + meshFilename = self.get_mesh_filename() with xr.open_dataset(meshFilename) as dsMesh: refBottomDepth = dsMesh.refBottomDepth diff --git a/mpas_analysis/ocean/ocean_regional_profiles.py b/mpas_analysis/ocean/ocean_regional_profiles.py index 0133d7878..7f7c6b57b 100644 --- a/mpas_analysis/ocean/ocean_regional_profiles.py +++ b/mpas_analysis/ocean/ocean_regional_profiles.py @@ -344,7 +344,7 @@ def run_task(self): return # get areaCell - meshFilename = self.runStreams.readpath('mesh')[0] + meshFilename = self.get_mesh_filename() dsMesh = xr.open_dataset(meshFilename) dsMesh = dsMesh.isel(Time=0) @@ -435,13 +435,7 @@ def run_task(self): # Note: restart file, not a mesh file because we need refBottomDepth, # not in a mesh file - try: - meshFilename = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'MPAS-O mesh file not found: needed for plotting time series ' - 'vs. depth' - ) + meshFilename = self.get_mesh_filename() with xr.open_dataset(meshFilename) as dsMesh: depths = dsMesh.refBottomDepth.values diff --git a/mpas_analysis/ocean/plot_hovmoller_subtask.py b/mpas_analysis/ocean/plot_hovmoller_subtask.py index 36cf45a24..d5898ba23 100644 --- a/mpas_analysis/ocean/plot_hovmoller_subtask.py +++ b/mpas_analysis/ocean/plot_hovmoller_subtask.py @@ -261,13 +261,7 @@ def run_task(self): # Note: mesh file, not a mesh file because we need refBottomDepth, # not in a mesh file - try: - meshFilename = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'The MPAS-O mesh file was not found: needed for ' - 'plotting time series vs. depth' - ) + meshFilename = self.get_mesh_filename() # Define/read in general variables self.logger.info(' Read in depth...') diff --git a/mpas_analysis/ocean/regional_ts_diagrams.py b/mpas_analysis/ocean/regional_ts_diagrams.py index 3bf41f124..300e1dcaa 100644 --- a/mpas_analysis/ocean/regional_ts_diagrams.py +++ b/mpas_analysis/ocean/regional_ts_diagrams.py @@ -600,11 +600,7 @@ def _write_mpas_t_s(self, config): cellsChunk = 32768 chunk = {'nCells': cellsChunk} - try: - meshFilename = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError('No MPAS-O mesh file found: need at least one' - ' mesh file to plot T-S diagrams') + meshFilename = self.get_mesh_filename() dsMesh = xarray.open_dataset(meshFilename) dsMesh = dsMesh.isel(Time=0) if 'landIceMask' in dsMesh: diff --git a/mpas_analysis/ocean/streamfunction_moc.py b/mpas_analysis/ocean/streamfunction_moc.py index 758ce988e..ee559dadd 100644 --- a/mpas_analysis/ocean/streamfunction_moc.py +++ b/mpas_analysis/ocean/streamfunction_moc.py @@ -362,13 +362,7 @@ def _compute_moc_climo_analysismember(self): regionNames.append('Global') # Read in depth and bin latitudes - try: - meshFilename = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'The MPAS-O mesh file could not be found: needed for MOC ' - 'calculation' - ) + meshFilename = self.get_mesh_filename() with xr.open_dataset(meshFilename) as dsMesh: refBottomDepth = dsMesh.refBottomDepth.values @@ -492,7 +486,7 @@ def _compute_moc_climo_postprocess(self): dvEdge, areaCell, refBottomDepth, latCell, nVertLevels, \ refTopDepth, refLayerThickness, cellsOnEdge = \ - _load_mesh(self.runStreams) + _load_mesh(self.get_mesh_filename()) regionNames = config.getexpression(self.sectionName, 'regionNames') @@ -1085,13 +1079,7 @@ def _compute_moc_time_series_analysismember(self): moc = np.zeros((len(inputFiles), sizes['nVertLevels']+1, len(binBoundaryMocStreamfunction))) - try: - meshFilename = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'The MPAS-O mesh file could not be found: needed for ' - 'MOC calculation' - ) + meshFilename = self.get_mesh_filename() with xr.open_dataset(meshFilename) as dsMesh: refBottomDepth = dsMesh.refBottomDepth.values @@ -1166,7 +1154,7 @@ def _compute_moc_time_series_postprocess(self): dvEdge, areaCell, refBottomDepth, latCell, nVertLevels, \ refTopDepth, refLayerThickness, cellsOnEdge = \ - _load_mesh(self.runStreams) + _load_mesh(self.get_mesh_filename()) mpasMeshName = config.get('input', 'mpasMeshName') @@ -1576,17 +1564,8 @@ def _load_moc(self, config): return dsMOCTimeSeries -def _load_mesh(runStreams): +def _load_mesh(meshFilename): # Load mesh related variables - - try: - meshFilename = runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'The MPAS-O mesh file could not be found: needed for ' - 'MOC calculation' - ) - ncFile = netCDF4.Dataset(meshFilename, mode='r') dvEdge = ncFile.variables['dvEdge'][:] areaCell = ncFile.variables['areaCell'][:] diff --git a/mpas_analysis/ocean/time_series_antarctic_melt.py b/mpas_analysis/ocean/time_series_antarctic_melt.py index 1cbc06a0b..65dff877d 100644 --- a/mpas_analysis/ocean/time_series_antarctic_melt.py +++ b/mpas_analysis/ocean/time_series_antarctic_melt.py @@ -276,8 +276,7 @@ def run_task(self): f'Deleting it.') os.remove(outFileName) - meshFilename = \ - mpasTimeSeriesTask.runStreams.readpath('mesh')[0] + meshFilename = self.get_mesh_filename() dsMesh = xarray.open_dataset(meshFilename) landIceFraction = dsMesh.landIceFraction.isel(Time=0) diff --git a/mpas_analysis/ocean/time_series_ocean_regions.py b/mpas_analysis/ocean/time_series_ocean_regions.py index a56069a79..cd758e62d 100644 --- a/mpas_analysis/ocean/time_series_ocean_regions.py +++ b/mpas_analysis/ocean/time_series_ocean_regions.py @@ -295,13 +295,7 @@ def run_task(self): return # Load mesh related variables - try: - meshFilename = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'The MPAS-O mesh file could not be found: needed for ocean ' - 'region time series' - ) + meshFilename = self.get_mesh_filename() if config.has_option(sectionName, 'zmin'): config_zmin = config.getfloat(sectionName, 'zmin') diff --git a/mpas_analysis/ocean/time_series_ohc_anomaly.py b/mpas_analysis/ocean/time_series_ohc_anomaly.py index a4132f808..228fcaa5c 100644 --- a/mpas_analysis/ocean/time_series_ohc_anomaly.py +++ b/mpas_analysis/ocean/time_series_ohc_anomaly.py @@ -152,13 +152,7 @@ def _compute_ohc(self, ds): ds.ohc.attrs['units'] = '$10^{22}$ J' ds.ohc.attrs['description'] = 'Ocean heat content in each region' - try: - meshFile = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'The MPAS-O mesh file could not be found: needed for OHC ' - 'calculation' - ) + meshFile = self.get_mesh_filename() # Define/read in general variables with xr.open_dataset(meshFile) as dsMesh: diff --git a/mpas_analysis/ocean/time_series_transport.py b/mpas_analysis/ocean/time_series_transport.py index fe461fed6..188881d17 100644 --- a/mpas_analysis/ocean/time_series_transport.py +++ b/mpas_analysis/ocean/time_series_transport.py @@ -208,13 +208,7 @@ def setup_and_check(self): raiseException=True) # Load mesh related variables - try: - self.meshFilename = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'The MPAS-O mesh file was not found: needed for transport ' - 'calculations' - ) + self.meshFilename = self.get_mesh_filename() def run_task(self): """ diff --git a/mpas_analysis/sea_ice/time_series.py b/mpas_analysis/sea_ice/time_series.py index 12726bc56..0c4f3b08d 100644 --- a/mpas_analysis/sea_ice/time_series.py +++ b/mpas_analysis/sea_ice/time_series.py @@ -135,13 +135,7 @@ def setup_and_check(self): self.simulationStartTime = get_simulation_start_time(self.runStreams) - try: - self.meshFilename = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'The MPAS-seaice mesh file could not be found: needed to ' - 'compute sea ice time series.' - ) + self.meshFilename = self.get_mesh_filename() # these are redundant for now. Later cleanup is needed where these # file names are reused in run() diff --git a/mpas_analysis/shared/analysis_task.py b/mpas_analysis/shared/analysis_task.py index 21c480b2a..f66094d53 100644 --- a/mpas_analysis/shared/analysis_task.py +++ b/mpas_analysis/shared/analysis_task.py @@ -491,6 +491,27 @@ def set_start_end_date(self, section): self.config.set(section, 'endDate', endDate) + def get_mesh_filename(self): + """ + Get the name of the MPAS mesh file for this component. + + Returns + ------- + meshFilename : str + The name of the MPAS mesh file for this component + """ + # Authors + # ------- + # Xylar Asay-Davis + + meshStream = self.config.get(self.componentName, 'meshStream') + try: + return self.runStreams.readpath(meshStream)[0] + except ValueError: + raise IOError( + f'The MPAS mesh file could not be found: needed to ' + f'run {self.componentName} analysis') + # }}} diff --git a/mpas_analysis/shared/climatology/remap_mpas_climatology_subtask.py b/mpas_analysis/shared/climatology/remap_mpas_climatology_subtask.py index 2661992e1..702f7f56e 100644 --- a/mpas_analysis/shared/climatology/remap_mpas_climatology_subtask.py +++ b/mpas_analysis/shared/climatology/remap_mpas_climatology_subtask.py @@ -206,12 +206,7 @@ def setup_and_check(self): # self.calendar super(RemapMpasClimatologySubtask, self).setup_and_check() - try: - self.meshFilename = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'The MPAS mesh file could not be found: needed to perform ' - 'remapping of climatologies.') + self.meshFilename = self.get_mesh_filename() # we set up the remapper here because ESFM_RegridWeightGen seems to # have trouble if it runs in another process (or in several at once) diff --git a/mpas_analysis/shared/regions/compute_region_masks_subtask.py b/mpas_analysis/shared/regions/compute_region_masks_subtask.py index ebaccc8ec..0fb4e1524 100644 --- a/mpas_analysis/shared/regions/compute_region_masks_subtask.py +++ b/mpas_analysis/shared/regions/compute_region_masks_subtask.py @@ -268,13 +268,7 @@ def setup_and_check(self): super(ComputeRegionMasksSubtask, self).setup_and_check() if self.useMpasMesh: - try: - self.obsFileName = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'The MPAS mesh file could not be found: needed to perform ' - 'region masking.' - ) + self.obsFileName = self.get_mesh_filename() maskSubdirectory = build_config_full_path(self.config, 'output', 'maskSubdirectory') diff --git a/mpas_analysis/shared/transects/compute_transect_masks_subtask.py b/mpas_analysis/shared/transects/compute_transect_masks_subtask.py index 1cc6d4d75..63eb618e4 100644 --- a/mpas_analysis/shared/transects/compute_transect_masks_subtask.py +++ b/mpas_analysis/shared/transects/compute_transect_masks_subtask.py @@ -193,13 +193,7 @@ def setup_and_check(self): # self.calendar super(ComputeTransectMasksSubtask, self).setup_and_check() - try: - self.obsFileName = self.runStreams.readpath('mesh')[0] - except ValueError: - raise IOError( - 'The MPAS mesh file could not be found: needed to perform ' - 'transect masking.' - ) + self.obsFileName = self.get_mesh_filename() self.maskSubdirectory = build_config_full_path(self.config, 'output', 'maskSubdirectory') From 4ea36009e06ed50d1d7484e590fbdadeabca2a84 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 11 Aug 2025 06:10:31 -0500 Subject: [PATCH 079/116] Define minLevelCell in BSF mesh variables if missing --- mpas_analysis/ocean/climatology_map_bsf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mpas_analysis/ocean/climatology_map_bsf.py b/mpas_analysis/ocean/climatology_map_bsf.py index 44b62a283..f54c7ede7 100644 --- a/mpas_analysis/ocean/climatology_map_bsf.py +++ b/mpas_analysis/ocean/climatology_map_bsf.py @@ -332,6 +332,10 @@ def customize_masked_climatology(self, climatology, season): 'latVertex', 'areaTriangle', ] + if 'minLevelCell' not in ds_mesh: + # some older meshes don't have this one + ds_mesh['minLevelCell'] = xr.ones_like(ds_mesh.maxLevelCell) + ds_mesh = ds_mesh[var_list].as_numpy() masked_filename = self.get_masked_file_name(season) From 8a3708ac5defb003d45e675b3b011d71d950d992 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 11 Aug 2025 13:20:36 +0200 Subject: [PATCH 080/116] Add ocean mesh stream to test config file --- mpas_analysis/test/test_mpas_climatology_task/QU240.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mpas_analysis/test/test_mpas_climatology_task/QU240.cfg b/mpas_analysis/test/test_mpas_climatology_task/QU240.cfg index aa6607e78..485a014ad 100644 --- a/mpas_analysis/test/test_mpas_climatology_task/QU240.cfg +++ b/mpas_analysis/test/test_mpas_climatology_task/QU240.cfg @@ -37,3 +37,6 @@ mpasInterpolationMethod = bilinear useNcclimo = True useNcremap = True renormalizationThreshold = 0.01 + +[ocean] +meshStream = mesh From 5c77cacaf12dc1bcf2ef413933ce8c867ca816b8 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 11 Aug 2025 07:43:01 -0500 Subject: [PATCH 081/116] Check if mesh file exists and fall back on restart stream If neither the file in the mesh stream nor the first file matching the pattern for the restart file exists, raise an error. --- mpas_analysis/shared/analysis_task.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/mpas_analysis/shared/analysis_task.py b/mpas_analysis/shared/analysis_task.py index f66094d53..7a5a4d8c7 100644 --- a/mpas_analysis/shared/analysis_task.py +++ b/mpas_analysis/shared/analysis_task.py @@ -19,6 +19,7 @@ import time import traceback import logging +import os import sys from mpas_analysis.shared.io import NameList, StreamsFile @@ -506,11 +507,23 @@ def get_mesh_filename(self): meshStream = self.config.get(self.componentName, 'meshStream') try: - return self.runStreams.readpath(meshStream)[0] + meshFilename = self.runStreams.readpath(meshStream)[0] except ValueError: + meshFilename = None + + if meshFilename is None or not os.path.exists(meshFilename): + # try again with "restart" stream + try: + meshFilename = self.runStreams.readpath('restart')[0] + except ValueError: + meshFilename = None + + if meshFilename is None or not os.path.exists(meshFilename): raise IOError( - f'The MPAS mesh file could not be found: needed to ' - f'run {self.componentName} analysis') + f'The MPAS mesh file could not be found via either ' + f'"{meshStream}" or "restart" streams') + + return meshFilename # }}} From b2eeccd470490d7acb4cf2784a571ffd68ebd3cb Mon Sep 17 00:00:00 2001 From: Irena Vankova Date: Thu, 21 Aug 2025 15:17:48 -0600 Subject: [PATCH 082/116] removed map inset --- mpas_analysis/ocean/time_series_antarctic_melt.py | 4 ++-- mpas_analysis/ocean/time_series_ocean_regions.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mpas_analysis/ocean/time_series_antarctic_melt.py b/mpas_analysis/ocean/time_series_antarctic_melt.py index 65dff877d..14668a109 100644 --- a/mpas_analysis/ocean/time_series_antarctic_melt.py +++ b/mpas_analysis/ocean/time_series_antarctic_melt.py @@ -723,7 +723,7 @@ def run_task(self): # and cartopy doesn't play too well with tight_layout anyway plt.tight_layout() - add_inset(fig, fc, width=2.0, height=2.0) + #add_inset(fig, fc, width=2.0, height=2.0) savefig(outFileName, config) @@ -788,7 +788,7 @@ def run_task(self): # and cartopy doesn't play too well with tight_layout anyway plt.tight_layout() - add_inset(fig, fc, width=2.0, height=2.0) + #add_inset(fig, fc, width=2.0, height=2.0) savefig(outFileName, config) diff --git a/mpas_analysis/ocean/time_series_ocean_regions.py b/mpas_analysis/ocean/time_series_ocean_regions.py index cd758e62d..edfe20c50 100644 --- a/mpas_analysis/ocean/time_series_ocean_regions.py +++ b/mpas_analysis/ocean/time_series_ocean_regions.py @@ -1350,7 +1350,7 @@ def run_task(self): # and cartopy doesn't play too well with tight_layout anyway plt.tight_layout() - add_inset(fig, fc, width=2.0, height=2.0) + #add_inset(fig, fc, width=2.0, height=2.0) savefig(outFileName, config, tight=False) From 4ef8c83c938eb86780e84e958f2b64f2e029640d Mon Sep 17 00:00:00 2001 From: Irena Vankova Date: Thu, 21 Aug 2025 15:56:14 -0600 Subject: [PATCH 083/116] moved inset like in hov --- mpas_analysis/ocean/ocean_regional_profiles.py | 11 ++++++++++- mpas_analysis/ocean/time_series_antarctic_melt.py | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/mpas_analysis/ocean/ocean_regional_profiles.py b/mpas_analysis/ocean/ocean_regional_profiles.py index 7f7c6b57b..797128e96 100644 --- a/mpas_analysis/ocean/ocean_regional_profiles.py +++ b/mpas_analysis/ocean/ocean_regional_profiles.py @@ -349,6 +349,8 @@ def run_task(self): dsMesh = xr.open_dataset(meshFilename) dsMesh = dsMesh.isel(Time=0) areaCell = dsMesh.areaCell + landIceFloatingMask = dsMesh.landIceFloatingMask.isel(Time=0) + landIceFloatingMask = -1*(landIceFloatingMask-1) nVertLevels = dsMesh.sizes['nVertLevels'] @@ -407,7 +409,14 @@ def run_task(self): prefix = field['prefix'] self.logger.info(' {}'.format(field['titleName'])) - var = dsLocal[variableName].where(vertDepthMask) + var_mpas = dsLocal[variableName] + print('DIMS-------------') + print(var_mpas.dims) + print(landIceFloatingMask.dims) + var_mpas_masked = var_mpas*landIceFloatingMask + print(var_mpas_masked.dims) + var = var_mpas_masked.where(vertDepthMask) + print(var.dims) meanName = '{}_mean'.format(prefix) dsLocal[meanName] = \ diff --git a/mpas_analysis/ocean/time_series_antarctic_melt.py b/mpas_analysis/ocean/time_series_antarctic_melt.py index 14668a109..ee1b09f15 100644 --- a/mpas_analysis/ocean/time_series_antarctic_melt.py +++ b/mpas_analysis/ocean/time_series_antarctic_melt.py @@ -724,6 +724,7 @@ def run_task(self): plt.tight_layout() #add_inset(fig, fc, width=2.0, height=2.0) + add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.1, ybuffer=0.1) savefig(outFileName, config) @@ -789,6 +790,7 @@ def run_task(self): plt.tight_layout() #add_inset(fig, fc, width=2.0, height=2.0) + add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.1, ybuffer=0.1) savefig(outFileName, config) From 04d638b2b2c23eb8db2a223e749883cb2f0dffb4 Mon Sep 17 00:00:00 2001 From: Irena Vankova Date: Thu, 21 Aug 2025 16:16:16 -0600 Subject: [PATCH 084/116] landIceFraction instead of landIceFloatingMask --- mpas_analysis/ocean/ocean_regional_profiles.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mpas_analysis/ocean/ocean_regional_profiles.py b/mpas_analysis/ocean/ocean_regional_profiles.py index 797128e96..3f052f4cf 100644 --- a/mpas_analysis/ocean/ocean_regional_profiles.py +++ b/mpas_analysis/ocean/ocean_regional_profiles.py @@ -349,8 +349,9 @@ def run_task(self): dsMesh = xr.open_dataset(meshFilename) dsMesh = dsMesh.isel(Time=0) areaCell = dsMesh.areaCell - landIceFloatingMask = dsMesh.landIceFloatingMask.isel(Time=0) - landIceFloatingMask = -1*(landIceFloatingMask-1) + landIceFraction = dsMesh.landIceFraction.isel(Time=0) + landIceFraction = xr.where(landIceFraction > 0, 1, landIceFraction) + landIceFraction = -1*(landIceFraction-1) nVertLevels = dsMesh.sizes['nVertLevels'] @@ -412,8 +413,8 @@ def run_task(self): var_mpas = dsLocal[variableName] print('DIMS-------------') print(var_mpas.dims) - print(landIceFloatingMask.dims) - var_mpas_masked = var_mpas*landIceFloatingMask + print(landIceFraction.dims) + var_mpas_masked = var_mpas*landIceFraction print(var_mpas_masked.dims) var = var_mpas_masked.where(vertDepthMask) print(var.dims) From 339013d2e71494e968050cd5b7759341972dbb87 Mon Sep 17 00:00:00 2001 From: Irena Vankova Date: Thu, 21 Aug 2025 16:31:09 -0600 Subject: [PATCH 085/116] landIceFraction no time --- mpas_analysis/ocean/ocean_regional_profiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpas_analysis/ocean/ocean_regional_profiles.py b/mpas_analysis/ocean/ocean_regional_profiles.py index 3f052f4cf..04497add0 100644 --- a/mpas_analysis/ocean/ocean_regional_profiles.py +++ b/mpas_analysis/ocean/ocean_regional_profiles.py @@ -349,7 +349,7 @@ def run_task(self): dsMesh = xr.open_dataset(meshFilename) dsMesh = dsMesh.isel(Time=0) areaCell = dsMesh.areaCell - landIceFraction = dsMesh.landIceFraction.isel(Time=0) + landIceFraction = dsMesh.landIceFraction landIceFraction = xr.where(landIceFraction > 0, 1, landIceFraction) landIceFraction = -1*(landIceFraction-1) From 841e8918cb92056e139b9675f6bdf6b97f8dabd1 Mon Sep 17 00:00:00 2001 From: Irena Vankova Date: Fri, 22 Aug 2025 12:21:50 -0600 Subject: [PATCH 086/116] shift inset --- mpas_analysis/ocean/time_series_antarctic_melt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mpas_analysis/ocean/time_series_antarctic_melt.py b/mpas_analysis/ocean/time_series_antarctic_melt.py index ee1b09f15..ec56a4a8e 100644 --- a/mpas_analysis/ocean/time_series_antarctic_melt.py +++ b/mpas_analysis/ocean/time_series_antarctic_melt.py @@ -724,7 +724,7 @@ def run_task(self): plt.tight_layout() #add_inset(fig, fc, width=2.0, height=2.0) - add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.1, ybuffer=0.1) + add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.3, ybuffer=0.3) savefig(outFileName, config) @@ -790,7 +790,7 @@ def run_task(self): plt.tight_layout() #add_inset(fig, fc, width=2.0, height=2.0) - add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.1, ybuffer=0.1) + add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.3, ybuffer=0.3) savefig(outFileName, config) From 52e578425354e4063f6073671982ccfc1770a809 Mon Sep 17 00:00:00 2001 From: Irena Vankova Date: Fri, 22 Aug 2025 12:27:25 -0600 Subject: [PATCH 087/116] GT to Gt --- mpas_analysis/ocean/time_series_antarctic_melt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mpas_analysis/ocean/time_series_antarctic_melt.py b/mpas_analysis/ocean/time_series_antarctic_melt.py index ec56a4a8e..73a60dee9 100644 --- a/mpas_analysis/ocean/time_series_antarctic_melt.py +++ b/mpas_analysis/ocean/time_series_antarctic_melt.py @@ -342,7 +342,7 @@ def run_task(self): dsOut = xarray.concat(objs=datasets, dim='Time') dsOut['regionNames'] = dsRegionMask.regionNames - dsOut.integratedMeltFlux.attrs['units'] = 'GT a$^{-1}$' + dsOut.integratedMeltFlux.attrs['units'] = 'Gt a$^{-1}$' dsOut.integratedMeltFlux.attrs['description'] = \ 'Integrated melt flux summed over each ice shelf or region' dsOut.meltRates.attrs['units'] = 'm a$^{-1}$' @@ -662,7 +662,7 @@ def run_task(self): suffix = self.iceShelf.replace(' ', '_') xLabel = 'Time (yr)' - yLabel = 'Melt Flux (GT/yr)' + yLabel = 'Melt Flux (Gt/yr)' timeSeries = integratedMeltFlux.isel(nRegions=self.regionIndex) From d75e26851856dd802a38e1b2d34c846d8e518720 Mon Sep 17 00:00:00 2001 From: Irena Vankova Date: Fri, 22 Aug 2025 12:29:20 -0600 Subject: [PATCH 088/116] less inset buffer --- mpas_analysis/ocean/time_series_antarctic_melt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mpas_analysis/ocean/time_series_antarctic_melt.py b/mpas_analysis/ocean/time_series_antarctic_melt.py index 73a60dee9..4257f158f 100644 --- a/mpas_analysis/ocean/time_series_antarctic_melt.py +++ b/mpas_analysis/ocean/time_series_antarctic_melt.py @@ -724,7 +724,7 @@ def run_task(self): plt.tight_layout() #add_inset(fig, fc, width=2.0, height=2.0) - add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.3, ybuffer=0.3) + add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.01, ybuffer=0.01) savefig(outFileName, config) @@ -790,7 +790,7 @@ def run_task(self): plt.tight_layout() #add_inset(fig, fc, width=2.0, height=2.0) - add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.3, ybuffer=0.3) + add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.01, ybuffer=0.01) savefig(outFileName, config) From 516841b72fb93efd722279c7381ed13eb3fbddae Mon Sep 17 00:00:00 2001 From: Irena Vankova Date: Fri, 22 Aug 2025 12:50:07 -0600 Subject: [PATCH 089/116] remove print statements --- mpas_analysis/ocean/ocean_regional_profiles.py | 5 ----- mpas_analysis/ocean/time_series_ocean_regions.py | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/mpas_analysis/ocean/ocean_regional_profiles.py b/mpas_analysis/ocean/ocean_regional_profiles.py index 04497add0..b5be6444e 100644 --- a/mpas_analysis/ocean/ocean_regional_profiles.py +++ b/mpas_analysis/ocean/ocean_regional_profiles.py @@ -411,13 +411,8 @@ def run_task(self): self.logger.info(' {}'.format(field['titleName'])) var_mpas = dsLocal[variableName] - print('DIMS-------------') - print(var_mpas.dims) - print(landIceFraction.dims) var_mpas_masked = var_mpas*landIceFraction - print(var_mpas_masked.dims) var = var_mpas_masked.where(vertDepthMask) - print(var.dims) meanName = '{}_mean'.format(prefix) dsLocal[meanName] = \ diff --git a/mpas_analysis/ocean/time_series_ocean_regions.py b/mpas_analysis/ocean/time_series_ocean_regions.py index edfe20c50..414af0831 100644 --- a/mpas_analysis/ocean/time_series_ocean_regions.py +++ b/mpas_analysis/ocean/time_series_ocean_regions.py @@ -1351,6 +1351,7 @@ def run_task(self): plt.tight_layout() #add_inset(fig, fc, width=2.0, height=2.0) + add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.01, ybuffer=0.01) savefig(outFileName, config, tight=False) From effdc971893b296be4d8216624fdae3cb2d883f9 Mon Sep 17 00:00:00 2001 From: Irena Vankova Date: Fri, 22 Aug 2025 13:02:35 -0600 Subject: [PATCH 090/116] inset lower left --- mpas_analysis/ocean/time_series_antarctic_melt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpas_analysis/ocean/time_series_antarctic_melt.py b/mpas_analysis/ocean/time_series_antarctic_melt.py index 4257f158f..ff460a45c 100644 --- a/mpas_analysis/ocean/time_series_antarctic_melt.py +++ b/mpas_analysis/ocean/time_series_antarctic_melt.py @@ -790,7 +790,7 @@ def run_task(self): plt.tight_layout() #add_inset(fig, fc, width=2.0, height=2.0) - add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.01, ybuffer=0.01) + add_inset(fig, fc, width=1.0, height=1.0, lowerleft=[0.0, 0.0], xbuffer=0.01, ybuffer=0.01) savefig(outFileName, config) From 0a825de53d0d36437e6bfa98b9ea61b373998486 Mon Sep 17 00:00:00 2001 From: Irena Vankova Date: Fri, 22 Aug 2025 13:08:12 -0600 Subject: [PATCH 091/116] inset lower left 2 --- mpas_analysis/ocean/time_series_antarctic_melt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpas_analysis/ocean/time_series_antarctic_melt.py b/mpas_analysis/ocean/time_series_antarctic_melt.py index ff460a45c..ad44b05cf 100644 --- a/mpas_analysis/ocean/time_series_antarctic_melt.py +++ b/mpas_analysis/ocean/time_series_antarctic_melt.py @@ -724,7 +724,7 @@ def run_task(self): plt.tight_layout() #add_inset(fig, fc, width=2.0, height=2.0) - add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.01, ybuffer=0.01) + add_inset(fig, fc, width=1.0, height=1.0, lowerleft=[0.0, 0.0], xbuffer=0.01, ybuffer=0.01) savefig(outFileName, config) From 558993473e6868b78bb0c285134c69eb3e29c4fc Mon Sep 17 00:00:00 2001 From: Irena Vankova Date: Fri, 22 Aug 2025 13:22:33 -0600 Subject: [PATCH 092/116] inset lower left volume integrated --- mpas_analysis/ocean/time_series_ocean_regions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpas_analysis/ocean/time_series_ocean_regions.py b/mpas_analysis/ocean/time_series_ocean_regions.py index 414af0831..c164f225f 100644 --- a/mpas_analysis/ocean/time_series_ocean_regions.py +++ b/mpas_analysis/ocean/time_series_ocean_regions.py @@ -1351,7 +1351,7 @@ def run_task(self): plt.tight_layout() #add_inset(fig, fc, width=2.0, height=2.0) - add_inset(fig, fc, width=1.0, height=1.0, xbuffer=0.01, ybuffer=0.01) + add_inset(fig, fc, width=1.0, height=1.0, lowerleft=[0.0, 0.0], xbuffer=0.01, ybuffer=0.01) savefig(outFileName, config, tight=False) From 929940aec1cca46610632a1780d61d3db033f4ad Mon Sep 17 00:00:00 2001 From: irenavankova Date: Mon, 25 Aug 2025 14:26:40 -0600 Subject: [PATCH 093/116] Update mpas_analysis/ocean/time_series_ocean_regions.py Co-authored-by: Xylar Asay-Davis --- mpas_analysis/ocean/time_series_ocean_regions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mpas_analysis/ocean/time_series_ocean_regions.py b/mpas_analysis/ocean/time_series_ocean_regions.py index c164f225f..c84f0dbc6 100644 --- a/mpas_analysis/ocean/time_series_ocean_regions.py +++ b/mpas_analysis/ocean/time_series_ocean_regions.py @@ -1350,7 +1350,6 @@ def run_task(self): # and cartopy doesn't play too well with tight_layout anyway plt.tight_layout() - #add_inset(fig, fc, width=2.0, height=2.0) add_inset(fig, fc, width=1.0, height=1.0, lowerleft=[0.0, 0.0], xbuffer=0.01, ybuffer=0.01) savefig(outFileName, config, tight=False) From e54dd45ca96d8331924d31c036b52e69cdfd7f7e Mon Sep 17 00:00:00 2001 From: irenavankova Date: Mon, 25 Aug 2025 14:26:51 -0600 Subject: [PATCH 094/116] Update mpas_analysis/ocean/time_series_antarctic_melt.py Co-authored-by: Xylar Asay-Davis --- mpas_analysis/ocean/time_series_antarctic_melt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mpas_analysis/ocean/time_series_antarctic_melt.py b/mpas_analysis/ocean/time_series_antarctic_melt.py index ad44b05cf..b80f05b0c 100644 --- a/mpas_analysis/ocean/time_series_antarctic_melt.py +++ b/mpas_analysis/ocean/time_series_antarctic_melt.py @@ -789,7 +789,6 @@ def run_task(self): # and cartopy doesn't play too well with tight_layout anyway plt.tight_layout() - #add_inset(fig, fc, width=2.0, height=2.0) add_inset(fig, fc, width=1.0, height=1.0, lowerleft=[0.0, 0.0], xbuffer=0.01, ybuffer=0.01) savefig(outFileName, config) From 1cbacf23d9b80ed76a0e673be845709c3081bd17 Mon Sep 17 00:00:00 2001 From: irenavankova Date: Mon, 25 Aug 2025 14:27:01 -0600 Subject: [PATCH 095/116] Update mpas_analysis/ocean/time_series_antarctic_melt.py Co-authored-by: Xylar Asay-Davis --- mpas_analysis/ocean/time_series_antarctic_melt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mpas_analysis/ocean/time_series_antarctic_melt.py b/mpas_analysis/ocean/time_series_antarctic_melt.py index b80f05b0c..9764fef20 100644 --- a/mpas_analysis/ocean/time_series_antarctic_melt.py +++ b/mpas_analysis/ocean/time_series_antarctic_melt.py @@ -723,7 +723,6 @@ def run_task(self): # and cartopy doesn't play too well with tight_layout anyway plt.tight_layout() - #add_inset(fig, fc, width=2.0, height=2.0) add_inset(fig, fc, width=1.0, height=1.0, lowerleft=[0.0, 0.0], xbuffer=0.01, ybuffer=0.01) savefig(outFileName, config) From b5e307e7699ef4c4cbeb9a06114c53b9b83cb217 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 28 Aug 2025 09:15:13 -0500 Subject: [PATCH 096/116] Remove icebergFreshWaterFlux from test suite It is not available in the test model run. --- suite/main.cfg | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/suite/main.cfg b/suite/main.cfg index b8909f14e..f66fdd462 100644 --- a/suite/main.cfg +++ b/suite/main.cfg @@ -17,3 +17,17 @@ variables = [ 'vertVisc', 'mixedLayerDepth' ] + +[climatologyMapMassFluxes] + +# excluding icebergFreshWaterFlux, which is not present in the test simulation +# output +variables = [ + 'riverRunoffFlux', + 'iceRunoffFlux', + 'snowFlux', + 'rainFlux', + 'evaporationFlux', + 'seaIceFreshWaterFlux', + 'landIceFreshwaterFlux' + ] From 712d22ca7c8f5ae968303c072f54e00e8082f90b Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 12 Sep 2025 17:57:54 +0200 Subject: [PATCH 097/116] Ignore .vscode settings --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e359827db..3512ba6ed 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,6 @@ ENV/ /chrysalis_test_suite/ /cori_test_suite/ /compy_test_suite/ + +# vscode settings +.vscode/ \ No newline at end of file From b326b4e863ae4e1d87601c32a7d127ff179b1580 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 12 Sep 2025 17:58:33 +0200 Subject: [PATCH 098/116] Remove six as a dependency --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e663ea1fc..3ace49a04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,6 @@ dependencies = [ "scipy>=1.7.0", "setuptools", "shapely>=2.0,<3.0", - "six", "xarray>=0.14.1" ] From 3479be8c8ed631f9c5e52fd623afb250b78429e4 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 12 Sep 2025 17:59:34 +0200 Subject: [PATCH 099/116] Update email --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3ace49a04..6d1fe2d2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "mpas_analysis" dynamic = ["version"] authors = [ - { name="Xylar Asay-Davis", email="xylar@lanl.gov" }, + { name="Xylar Asay-Davis", email="xylarstorm@gmail.com" }, { name="Carolyn Begeman" }, { name="Phillip J. Wolfram" }, { name="Milena Veneziani" }, From cd61b868172b62197d25bfe0fc59a5dde3616d58 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 12 Sep 2025 18:00:56 +0200 Subject: [PATCH 100/116] Fix project name --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6d1fe2d2d..15b6761e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "mpas_analysis" +name = "mpas-analysis" dynamic = ["version"] authors = [ { name="Xylar Asay-Davis", email="xylarstorm@gmail.com" }, From 711dff4a1c5df88de7e04a0cfa63b255e52fbec7 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 12 Sep 2025 18:04:13 +0200 Subject: [PATCH 101/116] Fix spacing and constrain setuptools --- dev-spec.txt | 2 +- pyproject.toml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev-spec.txt b/dev-spec.txt index 2f5b4b13e..d9ded6752 100644 --- a/dev-spec.txt +++ b/dev-spec.txt @@ -27,7 +27,7 @@ pyremap >=2.0.0,<3.0.0 python-dateutil requests scipy >=1.7.0 -setuptools +setuptools >=60 shapely>=2.0,<3.0 xarray>=0.14.1 diff --git a/pyproject.toml b/pyproject.toml index 15b6761e3..5bf7b864b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ classifiers = [ ] dependencies = [ - "cartopy>=0.18.0", + "cartopy >=0.18.0", "cmocean", "dask", "gsw", @@ -59,10 +59,10 @@ dependencies = [ "pyproj", "python-dateutil", "requests", - "scipy>=1.7.0", - "setuptools", - "shapely>=2.0,<3.0", - "xarray>=0.14.1" + "scipy >=1.7.0", + "setuptools >=60", + "shapely >=2.0,<3.0", + "xarray >=0.14.1" ] [project.optional-dependencies] From 7fbaf6bfb740f123f1b1086731f1c60828cf1c58 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 12 Sep 2025 18:05:52 +0200 Subject: [PATCH 102/116] Remove setuptools from run dependencies --- dev-spec.txt | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dev-spec.txt b/dev-spec.txt index d9ded6752..1b91ec65e 100644 --- a/dev-spec.txt +++ b/dev-spec.txt @@ -27,13 +27,13 @@ pyremap >=2.0.0,<3.0.0 python-dateutil requests scipy >=1.7.0 -setuptools >=60 shapely>=2.0,<3.0 xarray>=0.14.1 # Development pip pytest +setuptools >=60 # Documentation mock diff --git a/pyproject.toml b/pyproject.toml index 5bf7b864b..dec1ee9e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,6 @@ dependencies = [ "python-dateutil", "requests", "scipy >=1.7.0", - "setuptools >=60", "shapely >=2.0,<3.0", "xarray >=0.14.1" ] From 7573103fb243dc0fbea428405cf7732d6761bc96 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 12 Sep 2025 18:13:43 +0200 Subject: [PATCH 103/116] Drop python 3.9 --- .github/workflows/build_workflow.yml | 2 +- ci/python3.9.yaml | 8 -------- dev-spec.txt | 2 +- pyproject.toml | 3 +-- 4 files changed, 3 insertions(+), 12 deletions(-) delete mode 100644 ci/python3.9.yaml diff --git a/.github/workflows/build_workflow.yml b/.github/workflows/build_workflow.yml index 6291110d7..138431ba8 100644 --- a/.github/workflows/build_workflow.yml +++ b/.github/workflows/build_workflow.yml @@ -27,7 +27,7 @@ jobs: shell: bash -l {0} strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] fail-fast: false steps: - id: skip_check diff --git a/ci/python3.9.yaml b/ci/python3.9.yaml deleted file mode 100644 index 7929b1920..000000000 --- a/ci/python3.9.yaml +++ /dev/null @@ -1,8 +0,0 @@ -channel_sources: -- conda-forge,defaults -pin_run_as_build: - python: - min_pin: x.x - max_pin: x.x -python: -- 3.9.* *_cpython diff --git a/dev-spec.txt b/dev-spec.txt index 1b91ec65e..d77ffc6ef 100644 --- a/dev-spec.txt +++ b/dev-spec.txt @@ -2,7 +2,7 @@ # $ conda create --name --file # Base -python>=3.9 +python>=3.10 cartopy >=0.18.0 cartopy_offlinedata cmocean diff --git a/pyproject.toml b/pyproject.toml index dec1ee9e7..779d74769 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,10 +27,9 @@ description = """\ """ license = { file = "LICENSE" } readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ # these are only for searching/browsing projects on PyPI - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", From 3fb0fcd51b7412fc61f432d2dc8c50a4e010f07e Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 12 Sep 2025 18:13:53 +0200 Subject: [PATCH 104/116] Update conda recipe based on feedstock --- ci/recipe/meta.yaml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/ci/recipe/meta.yaml b/ci/recipe/meta.yaml index 700b3987d..37743405b 100644 --- a/ci/recipe/meta.yaml +++ b/ci/recipe/meta.yaml @@ -1,5 +1,6 @@ {% set name = "MPAS-Analysis" %} {% set version = "1.13.0" %} +{% set python_min = "3.10" %} package: name: {{ name|lower }} @@ -9,23 +10,25 @@ source: path: ../.. build: - number: 0 - script: {{ PYTHON }} -m pip install --no-deps --no-build-isolation -vv . - noarch: python + number: 0 + script: {{ PYTHON }} -m pip install . --no-deps --no-build-isolation -vv + noarch: python + entry_points: + - mpas_analysis = mpas_analysis.__main__:main + - download_analysis_data = mpas_analysis.download_data:download_analysis_data requirements: host: - - python >=3.9 + - python {{ python_min }} - pip - - setuptools + - setuptools >=60 run: - - python >=3.9 + - python >={{ python_min }},<3.13 - cartopy >=0.18.0 - cartopy_offlinedata - cmocean - dask - esmf >=8.4.2,<9.0.0 - - esmf=*=mpi_mpich_* - f90nml - geometric_features >=1.6.1 - gsw @@ -44,7 +47,6 @@ requirements: - python-dateutil - requests - scipy >=1.7.0 - - setuptools - shapely >=2.0,<3.0 - xarray >=0.14.1 @@ -52,6 +54,7 @@ test: requires: - pytest - pip + - python {{ python_min }} imports: - mpas_analysis - pytest @@ -78,5 +81,7 @@ about: extra: recipe-maintainers: + - andrewdnolan + - altheaden - xylar - - jhkennedy + - jhkennedy \ No newline at end of file From a000e8c3baf251de6f2bbd611130cd3ad4b08dfa Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 12 Sep 2025 18:16:52 +0200 Subject: [PATCH 105/116] Fix more spacing --- dev-spec.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dev-spec.txt b/dev-spec.txt index d77ffc6ef..507c390ee 100644 --- a/dev-spec.txt +++ b/dev-spec.txt @@ -2,7 +2,7 @@ # $ conda create --name --file # Base -python>=3.10 +python >=3.10 cartopy >=0.18.0 cartopy_offlinedata cmocean @@ -10,15 +10,15 @@ dask esmf >=8.4.2,<9.0.0 esmf=*=mpi_mpich_* f90nml -geometric_features>=1.6.1 +geometric_features >=1.6.1 gsw lxml mache >=1.11.0 -matplotlib-base>=3.9.0 +matplotlib-base >=3.9.0 mpas_tools >=1.3.0,<2.0.0 -nco>=4.8.1,!=5.2.6 +nco >=4.8.1,!=5.2.6 netcdf4 -numpy>=2.0,<3.0 +numpy >=2.0,<3.0 pandas pillow >=10.0.0,<11.0.0 progressbar2 @@ -27,8 +27,8 @@ pyremap >=2.0.0,<3.0.0 python-dateutil requests scipy >=1.7.0 -shapely>=2.0,<3.0 -xarray>=0.14.1 +shapely >=2.0,<3.0 +xarray >=0.14.1 # Development pip @@ -37,8 +37,8 @@ setuptools >=60 # Documentation mock -m2r2>=0.3.3 -mistune<2 +m2r2 >=0.3.3 +mistune <2 sphinx sphinx_rtd_theme tabulate From d3e20b33069ad52a8d37c01aaed05491e52b7a4a Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 12 Sep 2025 18:29:44 +0200 Subject: [PATCH 106/116] Update python in suite --- suite/run_dev_suite.bash | 4 ++-- suite/run_suite.bash | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/suite/run_dev_suite.bash b/suite/run_dev_suite.bash index 371666d09..f3db1df68 100755 --- a/suite/run_dev_suite.bash +++ b/suite/run_dev_suite.bash @@ -19,7 +19,7 @@ cd .. machine=$(python -c "from mache import discover_machine; print(discover_machine())") -py=3.11 +py=3.13 ./suite/setup.py -p ${py} -r main_py${py} -b ${branch} --copy_docs --clean -e ${env_name} ./suite/setup.py -p ${py} -r wc_defaults -b ${branch} --no_polar_regions -e ${env_name} ./suite/setup.py -p ${py} -r moc_am -b ${branch} -e ${env_name} @@ -33,7 +33,7 @@ py=3.11 # submit the jobs cd ${machine}_test_suite -main_py=3.11 +main_py=3.13 cd main_py${main_py} echo main_py${main_py} RES=$(sbatch job_script.bash) diff --git a/suite/run_suite.bash b/suite/run_suite.bash index 964103efe..ee8dd0b16 100755 --- a/suite/run_suite.bash +++ b/suite/run_suite.bash @@ -5,8 +5,8 @@ set -e conda_base=$(dirname $(dirname $CONDA_EXE)) source $conda_base/etc/profile.d/conda.sh -main_py=3.11 -alt_py=3.10 +main_py=3.13 +alt_py=3.12 export HDF5_USE_FILE_LOCKING=FALSE @@ -41,7 +41,7 @@ conda deactivate py=${main_py} conda activate test_mpas_analysis_py${py} cd docs -DOCS_VERSION=test make clean versioned-html +DOCS_VERSION=test make clean versioned-html cd .. machine=$(python -c "from mache import discover_machine; print(discover_machine())") From f5effb518d811f6fb59b455e864054235bd47f6a Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 12 Sep 2025 18:31:22 +0200 Subject: [PATCH 107/116] Switch from MpasConfigParser to tranche The main advantage of tranche is just that it's a standalone repo so packages can use it without needing MPAS-Tools but it also provides more testing and some additional functionality beyond the MpasConfigParser like interpolation of enviornment variables. --- ci/recipe/meta.yaml | 1 + dev-spec.txt | 1 + docs/tutorials/dev_add_task.rst | 8 +++---- docs/tutorials/dev_understand_a_task.rst | 12 +++++----- mpas_analysis/__main__.py | 16 ++++++------- mpas_analysis/analysis_task_template.py | 4 ++-- mpas_analysis/ocean/antship_transects.py | 6 ++--- .../ocean/climatology_map_antarctic_melt.py | 6 ++--- mpas_analysis/ocean/climatology_map_argo.py | 8 +++---- mpas_analysis/ocean/climatology_map_bgc.py | 4 ++-- mpas_analysis/ocean/climatology_map_bsf.py | 6 ++--- mpas_analysis/ocean/climatology_map_custom.py | 4 ++-- mpas_analysis/ocean/climatology_map_eke.py | 4 ++-- mpas_analysis/ocean/climatology_map_fluxes.py | 4 ++-- mpas_analysis/ocean/climatology_map_mld.py | 4 ++-- .../ocean/climatology_map_mld_min_max.py | 4 ++-- .../ocean/climatology_map_ohc_anomaly.py | 6 ++--- .../ocean/climatology_map_schmidtko.py | 4 ++-- mpas_analysis/ocean/climatology_map_sose.py | 4 ++-- mpas_analysis/ocean/climatology_map_ssh.py | 4 ++-- mpas_analysis/ocean/climatology_map_sss.py | 4 ++-- mpas_analysis/ocean/climatology_map_sst.py | 4 ++-- mpas_analysis/ocean/climatology_map_vel.py | 4 ++-- .../ocean/climatology_map_wind_stress_curl.py | 4 ++-- mpas_analysis/ocean/climatology_map_woa.py | 4 ++-- .../ocean/compute_transects_subtask.py | 4 ++-- mpas_analysis/ocean/conservation.py | 6 ++--- .../ocean/geojson_netcdf_transects.py | 6 ++--- mpas_analysis/ocean/histogram.py | 8 +++---- .../ocean/hovmoller_ocean_regions.py | 4 ++-- mpas_analysis/ocean/index_nino34.py | 6 ++--- .../ocean/meridional_heat_transport.py | 6 ++--- .../ocean/ocean_regional_profiles.py | 8 +++---- mpas_analysis/ocean/osnap_transects.py | 6 ++--- ...ot_depth_integrated_time_series_subtask.py | 4 ++-- mpas_analysis/ocean/plot_hovmoller_subtask.py | 4 ++-- mpas_analysis/ocean/plot_transect_subtask.py | 4 ++-- mpas_analysis/ocean/regional_ts_diagrams.py | 20 ++++++++-------- mpas_analysis/ocean/sose_transects.py | 12 +++++----- mpas_analysis/ocean/streamfunction_moc.py | 10 ++++---- .../ocean/time_series_antarctic_melt.py | 8 +++---- .../ocean/time_series_ocean_regions.py | 8 +++---- .../ocean/time_series_ohc_anomaly.py | 4 ++-- .../ocean/time_series_salinity_anomaly.py | 2 +- .../ocean/time_series_ssh_anomaly.py | 6 ++--- mpas_analysis/ocean/time_series_sst.py | 6 ++--- .../ocean/time_series_temperature_anomaly.py | 2 +- mpas_analysis/ocean/time_series_transport.py | 8 +++---- mpas_analysis/ocean/utility.py | 4 ++-- mpas_analysis/ocean/woa_transects.py | 10 ++++---- mpas_analysis/ocean/woce_transects.py | 6 ++--- .../sea_ice/climatology_map_albedo.py | 4 ++-- .../sea_ice/climatology_map_area_pond.py | 4 ++-- .../sea_ice/climatology_map_area_ridge.py | 4 ++-- .../sea_ice/climatology_map_berg_conc.py | 4 ++-- .../sea_ice/climatology_map_melting.py | 4 ++-- .../sea_ice/climatology_map_production.py | 4 ++-- .../sea_ice/climatology_map_sea_ice_conc.py | 4 ++-- .../sea_ice/climatology_map_sea_ice_thick.py | 4 ++-- .../sea_ice/climatology_map_snow_depth.py | 4 ++-- .../sea_ice/climatology_map_snowice.py | 4 ++-- .../sea_ice/climatology_map_snowmelt.py | 4 ++-- .../climatology_map_tendency_area_thermo.py | 4 ++-- .../climatology_map_tendency_area_transp.py | 4 ++-- .../climatology_map_tendency_volume_thermo.py | 4 ++-- .../climatology_map_tendency_volume_transp.py | 4 ++-- .../sea_ice/climatology_map_volume_ridge.py | 4 ++-- mpas_analysis/sea_ice/time_series.py | 6 ++--- mpas_analysis/shared/analysis_task.py | 4 ++-- .../shared/climatology/climatology.py | 12 +++++----- .../climatology/comparison_descriptors.py | 6 ++--- .../climatology/mpas_climatology_task.py | 2 +- .../ref_year_mpas_climatology_task.py | 4 ++-- .../generalized_reader/generalized_reader.py | 2 +- mpas_analysis/shared/html/image_xml.py | 2 +- mpas_analysis/shared/html/pages.py | 24 +++++++++---------- mpas_analysis/shared/io/utility.py | 6 ++--- mpas_analysis/shared/plot/climatology_map.py | 10 ++++---- mpas_analysis/shared/plot/colormap.py | 10 ++++---- .../plot/plot_climatology_map_subtask.py | 2 +- mpas_analysis/shared/plot/save.py | 2 +- .../shared/regions/compute_region_masks.py | 2 +- .../time_series/mpas_time_series_task.py | 2 +- mpas_analysis/test/test_analysis_task.py | 4 ++-- mpas_analysis/test/test_climatology.py | 4 ++-- mpas_analysis/test/test_generalized_reader.py | 4 ++-- .../test/test_mpas_climatology_task.py | 4 ++-- .../test/test_remap_obs_clim_subtask.py | 4 ++-- pyproject.toml | 1 + 89 files changed, 243 insertions(+), 240 deletions(-) diff --git a/ci/recipe/meta.yaml b/ci/recipe/meta.yaml index 37743405b..8db581370 100644 --- a/ci/recipe/meta.yaml +++ b/ci/recipe/meta.yaml @@ -48,6 +48,7 @@ requirements: - requests - scipy >=1.7.0 - shapely >=2.0,<3.0 + - tranche >=0.1.1 - xarray >=0.14.1 test: diff --git a/dev-spec.txt b/dev-spec.txt index 507c390ee..d29dbef05 100644 --- a/dev-spec.txt +++ b/dev-spec.txt @@ -28,6 +28,7 @@ python-dateutil requests scipy >=1.7.0 shapely >=2.0,<3.0 +tranche >=0.1.1 xarray >=0.14.1 # Development diff --git a/docs/tutorials/dev_add_task.rst b/docs/tutorials/dev_add_task.rst index 69539a078..4ae1c6f72 100644 --- a/docs/tutorials/dev_add_task.rst +++ b/docs/tutorials/dev_add_task.rst @@ -610,13 +610,13 @@ renaming the task are: Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask The task that produced the climatology to be remapped and plotted - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ @@ -1197,13 +1197,13 @@ described in this tutorial: Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask The task that produced the climatology to be remapped and plotted - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ diff --git a/docs/tutorials/dev_understand_a_task.rst b/docs/tutorials/dev_understand_a_task.rst index a02694f7f..154a0ee2d 100644 --- a/docs/tutorials/dev_understand_a_task.rst +++ b/docs/tutorials/dev_understand_a_task.rst @@ -242,7 +242,7 @@ super class's ``__init__()`` method: Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -252,7 +252,7 @@ super class's ``__init__()`` method: The task that produced the climatology from the first year to be remapped and then subtracted from the main climatology - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ @@ -303,7 +303,7 @@ find something unexpected: depth_ranges = config.getexpression('climatologyMapOHCAnomaly', 'depthRanges', - use_numpyfunc=True) + allow_numpy=True) By default, these config options look like this: @@ -904,7 +904,7 @@ here is the full analysis task as described in this tutorial: Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -914,7 +914,7 @@ here is the full analysis task as described in this tutorial: The task that produced the climatology from the first year to be remapped and then subtracted from the main climatology - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ @@ -946,7 +946,7 @@ here is the full analysis task as described in this tutorial: depth_ranges = config.getexpression('climatologyMapOHCAnomaly', 'depthRanges', - use_numpyfunc=True) + allow_numpy=True) mpas_field_name = 'deltaOHC' diff --git a/mpas_analysis/__main__.py b/mpas_analysis/__main__.py index 9d8f3250e..cab22ea39 100644 --- a/mpas_analysis/__main__.py +++ b/mpas_analysis/__main__.py @@ -37,7 +37,7 @@ from mache import discover_machine, MachineInfo -from mpas_tools.config import MpasConfigParser +from tranche import Tranche from mpas_analysis.shared.analysis_task import AnalysisFormatter @@ -70,7 +70,7 @@ def update_time_bounds_in_config(config): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche contains config options """ @@ -88,10 +88,10 @@ def build_analysis_list(config, controlConfig): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche contains config options - controlConfig : mpas_tools.config.MpasConfigParser or None + controlConfig : tranche.Tranche or None contains config options for a control run, or ``None`` if no config file for a control run was specified @@ -588,7 +588,7 @@ def update_generate(config, generate): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche contains config options generate : str @@ -615,7 +615,7 @@ def run_analysis(config, analyses): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche contains config options analyses : OrderedDict of ``AnalysisTask`` objects @@ -869,7 +869,7 @@ def build_config(user_config_file, shared_configs, machine_info): if not os.path.exists(user_config_file): raise OSError(f'A config file {user_config_file} was specified but ' f'the file does not exist') - config = MpasConfigParser() + config = Tranche() for config_file in shared_configs: if config_file.endswith('.py'): # we'll skip config options set in python files @@ -1053,7 +1053,7 @@ def main(): parser.print_help() sys.exit(0) - config = MpasConfigParser() + config = Tranche() # add default.cfg to cover default not included in the config files # provided on the command line diff --git a/mpas_analysis/analysis_task_template.py b/mpas_analysis/analysis_task_template.py index 503046c94..710e189d3 100644 --- a/mpas_analysis/analysis_task_template.py +++ b/mpas_analysis/analysis_task_template.py @@ -84,7 +84,7 @@ class MyTask(AnalysisTask): # python class start with the argument self, which is not included in # the list of arguments when you call a method of an object (because it # is always included automatically). - # config is an mpas_tools.config.MpasConfigParser object that can be used + # config is an tranche.Tranche object that can be used # to get configuration options stored in default.cfg or a custom config # file specific to a given simulation. See examples below or in # existing analysis tasks. @@ -103,7 +103,7 @@ def __init__(self, config, prerequsiteTask, myArg='myDefaultValue'): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options prerequsiteTask : ``AnotherTaskClass`` diff --git a/mpas_analysis/ocean/antship_transects.py b/mpas_analysis/ocean/antship_transects.py index d301761cb..723a17cdf 100644 --- a/mpas_analysis/ocean/antship_transects.py +++ b/mpas_analysis/ocean/antship_transects.py @@ -34,14 +34,14 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted as a transect - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -69,7 +69,7 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): verticalComparisonGrid = None else: verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', use_numpyfunc=True) + sectionName, 'verticalComparisonGrid', allow_numpy=True) verticalBounds = config.getexpression(sectionName, 'verticalBounds') diff --git a/mpas_analysis/ocean/climatology_map_antarctic_melt.py b/mpas_analysis/ocean/climatology_map_antarctic_melt.py index 122e9d3a5..32fd0acb7 100644 --- a/mpas_analysis/ocean/climatology_map_antarctic_melt.py +++ b/mpas_analysis/ocean/climatology_map_antarctic_melt.py @@ -50,7 +50,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` @@ -59,7 +59,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, regionMasksTask : ``ComputeRegionMasks`` A task for computing region masks - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche Configuration options for a control run """ # Authors @@ -460,7 +460,7 @@ def __init__(self, parentTask, mpasClimatologyTask, controlConfig, mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche Configuration options for a control run (if any) regionMasksTask : ``ComputeRegionMasks`` diff --git a/mpas_analysis/ocean/climatology_map_argo.py b/mpas_analysis/ocean/climatology_map_argo.py index 03dc857e4..aa92f98b9 100644 --- a/mpas_analysis/ocean/climatology_map_argo.py +++ b/mpas_analysis/ocean/climatology_map_argo.py @@ -49,13 +49,13 @@ def __init__(self, config, mpasClimatologyTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -189,13 +189,13 @@ def __init__(self, config, mpasClimatologyTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/climatology_map_bgc.py b/mpas_analysis/ocean/climatology_map_bgc.py index 0d7555a88..7703e3a73 100644 --- a/mpas_analysis/ocean/climatology_map_bgc.py +++ b/mpas_analysis/ocean/climatology_map_bgc.py @@ -37,13 +37,13 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) Authors diff --git a/mpas_analysis/ocean/climatology_map_bsf.py b/mpas_analysis/ocean/climatology_map_bsf.py index f54c7ede7..8cb812f05 100644 --- a/mpas_analysis/ocean/climatology_map_bsf.py +++ b/mpas_analysis/ocean/climatology_map_bsf.py @@ -40,13 +40,13 @@ def __init__(self, config, mpas_climatology_task, control_config=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask The task that produced the climatology to be remapped and plotted - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # noqa: E501 field_name = 'barotropicStreamfunction' @@ -64,7 +64,7 @@ def __init__(self, config, mpas_climatology_task, control_config=None): seasons = config.getexpression(section_name, 'seasons') depth_ranges = config.getexpression(section_name, 'depthRanges', - use_numpyfunc=True) + allow_numpy=True) if len(seasons) == 0: raise ValueError(f'config section {section_name} does not contain ' f'valid list of seasons') diff --git a/mpas_analysis/ocean/climatology_map_custom.py b/mpas_analysis/ocean/climatology_map_custom.py index 6e934b851..328144aa8 100644 --- a/mpas_analysis/ocean/climatology_map_custom.py +++ b/mpas_analysis/ocean/climatology_map_custom.py @@ -39,13 +39,13 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ diff --git a/mpas_analysis/ocean/climatology_map_eke.py b/mpas_analysis/ocean/climatology_map_eke.py index 28a75cc43..cd6d70ca3 100644 --- a/mpas_analysis/ocean/climatology_map_eke.py +++ b/mpas_analysis/ocean/climatology_map_eke.py @@ -37,13 +37,13 @@ def __init__(self, config, mpasClimatologyTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/climatology_map_fluxes.py b/mpas_analysis/ocean/climatology_map_fluxes.py index c55de400e..5e503d8e1 100644 --- a/mpas_analysis/ocean/climatology_map_fluxes.py +++ b/mpas_analysis/ocean/climatology_map_fluxes.py @@ -36,13 +36,13 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) fluxType : str, optional diff --git a/mpas_analysis/ocean/climatology_map_mld.py b/mpas_analysis/ocean/climatology_map_mld.py index 4b1b4de3d..3f49cd30e 100644 --- a/mpas_analysis/ocean/climatology_map_mld.py +++ b/mpas_analysis/ocean/climatology_map_mld.py @@ -38,13 +38,13 @@ def __init__(self, config, mpasClimatologyTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/climatology_map_mld_min_max.py b/mpas_analysis/ocean/climatology_map_mld_min_max.py index 1bc4b6b3c..63379dfc9 100644 --- a/mpas_analysis/ocean/climatology_map_mld_min_max.py +++ b/mpas_analysis/ocean/climatology_map_mld_min_max.py @@ -31,14 +31,14 @@ def __init__(self, config, mpasClimatologyTasks, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTasks : dict of ``MpasClimatologyTask`` The tasks that produced the climatology of monthly min and max to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/climatology_map_ohc_anomaly.py b/mpas_analysis/ocean/climatology_map_ohc_anomaly.py index e2e7eb2cb..672abe530 100644 --- a/mpas_analysis/ocean/climatology_map_ohc_anomaly.py +++ b/mpas_analysis/ocean/climatology_map_ohc_anomaly.py @@ -39,7 +39,7 @@ def __init__(self, config, mpas_climatology_task, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -49,7 +49,7 @@ def __init__(self, config, mpas_climatology_task, The task that produced the climatology from the first year to be remapped and then subtracted from the main climatology - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ @@ -81,7 +81,7 @@ def __init__(self, config, mpas_climatology_task, depth_ranges = config.getexpression('climatologyMapOHCAnomaly', 'depthRanges', - use_numpyfunc=True) + allow_numpy=True) mpas_field_name = 'deltaOHC' diff --git a/mpas_analysis/ocean/climatology_map_schmidtko.py b/mpas_analysis/ocean/climatology_map_schmidtko.py index 3320c94bc..be8d983c3 100644 --- a/mpas_analysis/ocean/climatology_map_schmidtko.py +++ b/mpas_analysis/ocean/climatology_map_schmidtko.py @@ -47,13 +47,13 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/climatology_map_sose.py b/mpas_analysis/ocean/climatology_map_sose.py index 366cd5e13..c5cc9ec5f 100644 --- a/mpas_analysis/ocean/climatology_map_sose.py +++ b/mpas_analysis/ocean/climatology_map_sose.py @@ -46,13 +46,13 @@ def __init__(self, config, mpasClimatologyTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/climatology_map_ssh.py b/mpas_analysis/ocean/climatology_map_ssh.py index f52f39794..4d7eb618c 100644 --- a/mpas_analysis/ocean/climatology_map_ssh.py +++ b/mpas_analysis/ocean/climatology_map_ssh.py @@ -39,13 +39,13 @@ def __init__(self, config, mpasClimatologyTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/climatology_map_sss.py b/mpas_analysis/ocean/climatology_map_sss.py index 2c074b4c2..d267ed72e 100644 --- a/mpas_analysis/ocean/climatology_map_sss.py +++ b/mpas_analysis/ocean/climatology_map_sss.py @@ -38,13 +38,13 @@ def __init__(self, config, mpasClimatologyTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/climatology_map_sst.py b/mpas_analysis/ocean/climatology_map_sst.py index d05dfeebe..97d75906e 100644 --- a/mpas_analysis/ocean/climatology_map_sst.py +++ b/mpas_analysis/ocean/climatology_map_sst.py @@ -39,13 +39,13 @@ def __init__(self, config, mpasClimatologyTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/climatology_map_vel.py b/mpas_analysis/ocean/climatology_map_vel.py index bdbfc9641..ede11385d 100644 --- a/mpas_analysis/ocean/climatology_map_vel.py +++ b/mpas_analysis/ocean/climatology_map_vel.py @@ -46,13 +46,13 @@ def __init__(self, config, mpasClimatologyTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/climatology_map_wind_stress_curl.py b/mpas_analysis/ocean/climatology_map_wind_stress_curl.py index 4925084c8..5f28a4872 100644 --- a/mpas_analysis/ocean/climatology_map_wind_stress_curl.py +++ b/mpas_analysis/ocean/climatology_map_wind_stress_curl.py @@ -35,13 +35,13 @@ def __init__(self, config, mpas_climatology_task, control_config=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask The task that produced the climatology to be remapped and plotted - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # noqa: E501 diff --git a/mpas_analysis/ocean/climatology_map_woa.py b/mpas_analysis/ocean/climatology_map_woa.py index 35eb03193..3ca549bdc 100644 --- a/mpas_analysis/ocean/climatology_map_woa.py +++ b/mpas_analysis/ocean/climatology_map_woa.py @@ -47,13 +47,13 @@ def __init__(self, config, mpasClimatologyTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/compute_transects_subtask.py b/mpas_analysis/ocean/compute_transects_subtask.py index c444130e4..b4b8ab0db 100644 --- a/mpas_analysis/ocean/compute_transects_subtask.py +++ b/mpas_analysis/ocean/compute_transects_subtask.py @@ -670,7 +670,7 @@ class TransectsObservations(object): Attributes ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options obsFileNames : OrderedDict @@ -700,7 +700,7 @@ def __init__(self, config, obsFileNames, horizontalResolution, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options obsFileNames : OrderedDict diff --git a/mpas_analysis/ocean/conservation.py b/mpas_analysis/ocean/conservation.py index 2b22a68d0..14f354f47 100644 --- a/mpas_analysis/ocean/conservation.py +++ b/mpas_analysis/ocean/conservation.py @@ -39,10 +39,10 @@ class ConservationTask(AnalysisTask): Attributes ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche Contains configuration options for a control run, if provided outputFile : str @@ -98,7 +98,7 @@ def __init__(self, config, controlConfig): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options """ # Authors diff --git a/mpas_analysis/ocean/geojson_netcdf_transects.py b/mpas_analysis/ocean/geojson_netcdf_transects.py index 203bbb222..6683257f2 100644 --- a/mpas_analysis/ocean/geojson_netcdf_transects.py +++ b/mpas_analysis/ocean/geojson_netcdf_transects.py @@ -38,14 +38,14 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted as a transect - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -78,7 +78,7 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): verticalComparisonGrid = None else: verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', use_numpyfunc=True) + sectionName, 'verticalComparisonGrid', allow_numpy=True) availableVariables = config.getexpression( sectionName, 'availableVariables') diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 3f99a2031..d7f94f9ad 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -42,7 +42,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` @@ -51,7 +51,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, regionMasksTask : ``ComputeRegionMasks`` A task for computing region masks - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche Configuration options for a control run (if any) """ @@ -293,7 +293,7 @@ class PlotRegionHistogramSubtask(AnalysisTask): sectionName : str The section of the config file to get options from - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche The configuration options for the control run (if any) mpasClimatologyTask : ``MpasClimatologyTask`` @@ -333,7 +333,7 @@ def __init__(self, parentTask, regionGroup, regionName, controlConfig, regionName : str Name of the region to plot - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) sectionName : str diff --git a/mpas_analysis/ocean/hovmoller_ocean_regions.py b/mpas_analysis/ocean/hovmoller_ocean_regions.py index 59aec3609..7a677f2e0 100644 --- a/mpas_analysis/ocean/hovmoller_ocean_regions.py +++ b/mpas_analysis/ocean/hovmoller_ocean_regions.py @@ -48,7 +48,7 @@ def __init__(self, config, regionMasksTask, oceanRegionalProfilesTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options regionMasksTask : ``ComputeRegionMasks`` @@ -57,7 +57,7 @@ def __init__(self, config, regionMasksTask, oceanRegionalProfilesTask, oceanRegionalProfilesTask : mpas_analysis.ocean.OceanRegionalProfiles A task for computing ocean regional profiles - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/index_nino34.py b/mpas_analysis/ocean/index_nino34.py index d4b018e70..b3cd49136 100644 --- a/mpas_analysis/ocean/index_nino34.py +++ b/mpas_analysis/ocean/index_nino34.py @@ -52,7 +52,7 @@ class IndexNino34(AnalysisTask): mpasTimeSeriesTask : ``MpasTimeSeriesTask`` The task that extracts the time series from MPAS monthly output - controlconfig : mpas_tools.config.MpasConfigParser + controlconfig : tranche.Tranche Configuration options for a control run (if any) """ # Authors @@ -66,13 +66,13 @@ def __init__(self, config, mpasTimeSeriesTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasTimeSeriesTask : ``MpasTimeSeriesTask`` The task that extracts the time series from MPAS monthly output - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/meridional_heat_transport.py b/mpas_analysis/ocean/meridional_heat_transport.py index 10f3b5590..6534a03dd 100644 --- a/mpas_analysis/ocean/meridional_heat_transport.py +++ b/mpas_analysis/ocean/meridional_heat_transport.py @@ -34,7 +34,7 @@ class MeridionalHeatTransport(AnalysisTask): mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser + controlconfig : tranche.Tranche Configuration options for a control run (if any) """ @@ -48,13 +48,13 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/ocean_regional_profiles.py b/mpas_analysis/ocean/ocean_regional_profiles.py index 7f7c6b57b..bba92e408 100644 --- a/mpas_analysis/ocean/ocean_regional_profiles.py +++ b/mpas_analysis/ocean/ocean_regional_profiles.py @@ -50,13 +50,13 @@ def __init__(self, config, regionMasksTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options regionMasksTask : ``ComputeRegionMasks`` A task for computing region masks - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -616,7 +616,7 @@ class PlotRegionalProfileTimeSeriesSubtask(AnalysisTask): field : dict Information about the field (e.g. temperature) being plotted - controlconfig : mpas_tools.config.MpasConfigParser + controlconfig : tranche.Tranche Configuration options for a control run (if any) """ # Authors @@ -653,7 +653,7 @@ def __init__(self, parentTask, masksSubtask, season, regionName, field, startYear, endYear : int The beginning and end of the time series to compute - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/osnap_transects.py b/mpas_analysis/ocean/osnap_transects.py index c04d5801b..e99c59001 100644 --- a/mpas_analysis/ocean/osnap_transects.py +++ b/mpas_analysis/ocean/osnap_transects.py @@ -33,14 +33,14 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted as a transect - controlConfig : mpas_tools.config.MpasConfigParser, optional + controlConfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -68,7 +68,7 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): verticalComparisonGrid = None else: verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', use_numpyfunc=True) + sectionName, 'verticalComparisonGrid', allow_numpy=True) verticalBounds = config.getexpression(sectionName, 'verticalBounds') diff --git a/mpas_analysis/ocean/plot_depth_integrated_time_series_subtask.py b/mpas_analysis/ocean/plot_depth_integrated_time_series_subtask.py index 57d18df16..4f2021dff 100644 --- a/mpas_analysis/ocean/plot_depth_integrated_time_series_subtask.py +++ b/mpas_analysis/ocean/plot_depth_integrated_time_series_subtask.py @@ -84,7 +84,7 @@ class PlotDepthIntegratedTimeSeriesSubtask(AnalysisTask): galleryName : str The name of the gallery in which this plot belongs - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche The configuration options for the control run (if any) """ # Authors @@ -150,7 +150,7 @@ def __init__(self, parentTask, regionName, inFileName, outFileLabel, subtaskName : str, optional The name of the subtask (``plotTimeSeries`` by default) - controlConfig : mpas_tools.config.MpasConfigParser, optional + controlConfig : tranche.Tranche, optional The configuration options for the control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/plot_hovmoller_subtask.py b/mpas_analysis/ocean/plot_hovmoller_subtask.py index d5898ba23..91a4fb70a 100644 --- a/mpas_analysis/ocean/plot_hovmoller_subtask.py +++ b/mpas_analysis/ocean/plot_hovmoller_subtask.py @@ -34,7 +34,7 @@ class PlotHovmollerSubtask(AnalysisTask): Attributes ---------- - controlconfig : mpas_tools.config.MpasConfigParser + controlconfig : tranche.Tranche Configuration options for a control run (if any) regionName : str @@ -148,7 +148,7 @@ def __init__(self, parentTask, regionName, inFileName, outFileLabel, subtaskName : str, optional The name of the subtask (``plotHovmoller`` by default) - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) regionMaskFile : str, optional diff --git a/mpas_analysis/ocean/plot_transect_subtask.py b/mpas_analysis/ocean/plot_transect_subtask.py index 22b187f60..1bb067b5b 100644 --- a/mpas_analysis/ocean/plot_transect_subtask.py +++ b/mpas_analysis/ocean/plot_transect_subtask.py @@ -56,7 +56,7 @@ class PlotTransectSubtask(AnalysisTask): plotObs : bool, optional Whether to plot against observations. - controlconfig : mpas_tools.config.MpasConfigParser + controlconfig : tranche.Tranche Configuration options for a control run (if any), ignored if ``plotObs == True`` @@ -133,7 +133,7 @@ def __init__(self, parentTask, season, transectName, fieldName, plotObs : bool, optional Whether to plot against observations. - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any), ignored if ``plotObs == True`` diff --git a/mpas_analysis/ocean/regional_ts_diagrams.py b/mpas_analysis/ocean/regional_ts_diagrams.py index 300e1dcaa..f808075a4 100644 --- a/mpas_analysis/ocean/regional_ts_diagrams.py +++ b/mpas_analysis/ocean/regional_ts_diagrams.py @@ -69,7 +69,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` @@ -78,7 +78,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, regionMasksTask : ``ComputeRegionMasks`` A task for computing region masks - controlConfig : mpas_tools.config.MpasConfigParser, optional + controlConfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -471,7 +471,7 @@ class ComputeRegionTSSubtask(AnalysisTask): sectionName : str The section of the config file to get options from - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche The configuration options for the control run (if any) mpasClimatologyTask : ``MpasClimatologyTask`` @@ -510,7 +510,7 @@ def __init__(self, parentTask, regionGroup, regionName, controlConfig, regionName : str Name of the region to plot - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) sectionName : str @@ -795,7 +795,7 @@ class PlotRegionTSDiagramSubtask(AnalysisTask): sectionName : str The section of the config file to get options from - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche The configuration options for the control run (if any) mpasClimatologyTask : ``MpasClimatologyTask`` @@ -834,7 +834,7 @@ def __init__(self, parentTask, regionGroup, regionName, controlConfig, regionName : str Name of the region to plot - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) sectionName : str @@ -1010,8 +1010,8 @@ def run_task(self): plotFields.append({'S': obsS, 'T': obsT, 'z': obsZ, 'vol': obsVol, 'title': obsName}) - Tbins = config.getexpression(sectionName, 'Tbins', use_numpyfunc=True) - Sbins = config.getexpression(sectionName, 'Sbins', use_numpyfunc=True) + Tbins = config.getexpression(sectionName, 'Tbins', allow_numpy=True) + Sbins = config.getexpression(sectionName, 'Sbins', allow_numpy=True) normType = config.get(sectionName, 'normType') @@ -1207,9 +1207,9 @@ def _plot_volumetric_panel(self, T, S, volume): sectionName = self.sectionName cmap = config.get(sectionName, 'colorMap') Tbins = config.getexpression(sectionName, 'Tbins', - use_numpyfunc=True) + allow_numpy=True) Sbins = config.getexpression(sectionName, 'Sbins', - use_numpyfunc=True) + allow_numpy=True) hist, _, _, panel = plt.hist2d(S, T, bins=[Sbins, Tbins], weights=volume, cmap=cmap, zorder=1, diff --git a/mpas_analysis/ocean/sose_transects.py b/mpas_analysis/ocean/sose_transects.py index 3fb2e67a4..e49fc30df 100644 --- a/mpas_analysis/ocean/sose_transects.py +++ b/mpas_analysis/ocean/sose_transects.py @@ -45,14 +45,14 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted as a transect - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -80,12 +80,12 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): verticalComparisonGrid = None else: verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', use_numpyfunc=True) + sectionName, 'verticalComparisonGrid', allow_numpy=True) verticalBounds = config.getexpression(sectionName, 'verticalBounds') longitudes = sorted(config.getexpression(sectionName, 'longitudes', - use_numpyfunc=True)) + allow_numpy=True)) fields = \ [{'prefix': 'temperature', @@ -236,7 +236,7 @@ def __init__(self, config, horizontalResolution, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options horizontalResolution : str @@ -296,7 +296,7 @@ def combine_observations(self): config = self.config longitudes = sorted(config.getexpression('soseTransects', 'longitudes', - use_numpyfunc=True)) + allow_numpy=True)) observationsDirectory = build_obs_path( config, 'ocean', 'soseSubdirectory') diff --git a/mpas_analysis/ocean/streamfunction_moc.py b/mpas_analysis/ocean/streamfunction_moc.py index ee559dadd..26ef1a32e 100644 --- a/mpas_analysis/ocean/streamfunction_moc.py +++ b/mpas_analysis/ocean/streamfunction_moc.py @@ -58,13 +58,13 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -639,7 +639,7 @@ def __init__(self, parentTask, controlConfig): parentTask : ``StreamfunctionMOC`` The main task of which this is a subtask - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -1344,7 +1344,7 @@ def __init__(self, parentTask, startYears, endYears): parentTask : ``StreamfunctionMOC`` The main task of which this is a subtask - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -1416,7 +1416,7 @@ def __init__(self, parentTask, controlConfig): parentTask : ``StreamfunctionMOC`` The main task of which this is a subtask - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/time_series_antarctic_melt.py b/mpas_analysis/ocean/time_series_antarctic_melt.py index 65dff877d..6daf54067 100644 --- a/mpas_analysis/ocean/time_series_antarctic_melt.py +++ b/mpas_analysis/ocean/time_series_antarctic_melt.py @@ -50,7 +50,7 @@ def __init__(self, config, mpasTimeSeriesTask, regionMasksTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasTimeSeriesTask : ``MpasTimeSeriesTask`` @@ -59,7 +59,7 @@ def __init__(self, config, mpasTimeSeriesTask, regionMasksTask, regionMasksTask : ``ComputeRegionMasks`` A task for computing region masks - controlConfig : mpas_tools.config.MpasConfigParser, optional + controlConfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -431,7 +431,7 @@ class PlotMeltSubtask(AnalysisTask): regionIndex : int The index into the dimension ``nRegions`` of the ice shelf to plot - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche The configuration options for the control run (if any) """ @@ -456,7 +456,7 @@ def __init__(self, parentTask, iceShelf, regionIndex, controlConfig): regionIndex : int The index into the dimension ``nRegions`` of the ice shelf to plot - controlConfig : mpas_tools.config.MpasConfigParser, optional + controlConfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/time_series_ocean_regions.py b/mpas_analysis/ocean/time_series_ocean_regions.py index cd758e62d..c2e5bff36 100644 --- a/mpas_analysis/ocean/time_series_ocean_regions.py +++ b/mpas_analysis/ocean/time_series_ocean_regions.py @@ -44,13 +44,13 @@ def __init__(self, config, regionMasksTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options regionMasksTask : ``ComputeRegionMasks`` A task for computing region masks - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -1046,7 +1046,7 @@ class PlotRegionTimeSeriesSubtask(AnalysisTask): sectionName : str The section of the config file to get options from - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche The configuration options for the control run (if any) """ @@ -1076,7 +1076,7 @@ def __init__(self, parentTask, regionGroup, regionName, regionIndex, regionIndex : int The index into the dimension ``nRegions`` of the region to plot - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) sectionName : str diff --git a/mpas_analysis/ocean/time_series_ohc_anomaly.py b/mpas_analysis/ocean/time_series_ohc_anomaly.py index 228fcaa5c..b3dbf21d0 100644 --- a/mpas_analysis/ocean/time_series_ohc_anomaly.py +++ b/mpas_analysis/ocean/time_series_ohc_anomaly.py @@ -43,13 +43,13 @@ def __init__(self, config, mpasTimeSeriesTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasTimeSeriesTask : ``MpasTimeSeriesTask`` The task that extracts the time series from MPAS monthly output - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/time_series_salinity_anomaly.py b/mpas_analysis/ocean/time_series_salinity_anomaly.py index 52290e897..e0dfd2376 100644 --- a/mpas_analysis/ocean/time_series_salinity_anomaly.py +++ b/mpas_analysis/ocean/time_series_salinity_anomaly.py @@ -34,7 +34,7 @@ def __init__(self, config, mpasTimeSeriesTask): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options mpasTimeSeriesTask : ``MpasTimeSeriesTask`` diff --git a/mpas_analysis/ocean/time_series_ssh_anomaly.py b/mpas_analysis/ocean/time_series_ssh_anomaly.py index 9e981e2cd..0043c06f2 100644 --- a/mpas_analysis/ocean/time_series_ssh_anomaly.py +++ b/mpas_analysis/ocean/time_series_ssh_anomaly.py @@ -36,7 +36,7 @@ class TimeSeriesSSHAnomaly(AnalysisTask): timeSeriesFileName : str The name of the file where the ssh anomaly is stored - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche Configuration options for a control run (if one is provided) filePrefix : str @@ -53,13 +53,13 @@ def __init__(self, config, mpasTimeSeriesTask, controlConfig): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasTimeSeriesTask : mpas_analysis.shared.time_series.MpasTimeSeriesTask The task that extracts the time series from MPAS monthly output - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/time_series_sst.py b/mpas_analysis/ocean/time_series_sst.py index d1fb3b649..7d95b8bec 100644 --- a/mpas_analysis/ocean/time_series_sst.py +++ b/mpas_analysis/ocean/time_series_sst.py @@ -40,7 +40,7 @@ class TimeSeriesSST(AnalysisTask): mpasTimeSeriesTask : ``MpasTimeSeriesTask`` The task that extracts the time series from MPAS monthly output - controlconfig : mpas_tools.config.MpasConfigParser + controlconfig : tranche.Tranche Configuration options for a control run (if any) """ # Authors @@ -54,13 +54,13 @@ def __init__(self, config, mpasTimeSeriesTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasTimeSeriesTask : ``MpasTimeSeriesTask`` The task that extracts the time series from MPAS monthly output - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/ocean/time_series_temperature_anomaly.py b/mpas_analysis/ocean/time_series_temperature_anomaly.py index 6dbd43f65..9469fcf77 100644 --- a/mpas_analysis/ocean/time_series_temperature_anomaly.py +++ b/mpas_analysis/ocean/time_series_temperature_anomaly.py @@ -34,7 +34,7 @@ def __init__(self, config, mpasTimeSeriesTask): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options mpasTimeSeriesTask : ``MpasTimeSeriesTask`` diff --git a/mpas_analysis/ocean/time_series_transport.py b/mpas_analysis/ocean/time_series_transport.py index 188881d17..e23996fd0 100644 --- a/mpas_analysis/ocean/time_series_transport.py +++ b/mpas_analysis/ocean/time_series_transport.py @@ -49,10 +49,10 @@ def __init__(self, config, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -464,7 +464,7 @@ class PlotTransportSubtask(AnalysisTask): transectIndex : int The index into the dimension ``nTransects`` of the transect to plot - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche The configuration options for the control run (if any) transportGroup : str (with spaces) @@ -495,7 +495,7 @@ def __init__(self, parentTask, transect, transectIndex, controlConfig, transectIndex : int The index into the dimension ``nTransects`` of the transect to plot - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) transportGroup : str (with spaces) diff --git a/mpas_analysis/ocean/utility.py b/mpas_analysis/ocean/utility.py index a95eda409..f6a6a8f3e 100644 --- a/mpas_analysis/ocean/utility.py +++ b/mpas_analysis/ocean/utility.py @@ -30,7 +30,7 @@ def add_standard_regions_and_subset(ds, config, regionShortNames=None): ds : xarray.Dataset the dataset to which region names should be added - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options regionShortNames : list of str, optional @@ -63,7 +63,7 @@ def get_standard_region_names(config, regionShortNames): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options regionShortNames : list of str diff --git a/mpas_analysis/ocean/woa_transects.py b/mpas_analysis/ocean/woa_transects.py index 83bd7a88f..f551b5e15 100644 --- a/mpas_analysis/ocean/woa_transects.py +++ b/mpas_analysis/ocean/woa_transects.py @@ -45,14 +45,14 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted as a transect - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -80,7 +80,7 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): verticalComparisonGrid = None else: verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', use_numpyfunc=True) + sectionName, 'verticalComparisonGrid', allow_numpy=True) verticalBounds = config.getexpression(sectionName, 'verticalBounds') @@ -202,7 +202,7 @@ def __init__(self, config, horizontalResolution, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options horizontalResolution : str @@ -363,7 +363,7 @@ def build_observational_dataset(self, fileName, transectName): def _get_longitudes(config): longitudes = config.getexpression('woaTransects', 'longitudes', - use_numpyfunc=True) + allow_numpy=True) longitudes = np.array(longitudes) # make sure longitudes are between -180 and 180 longitudes = np.sort(np.mod(longitudes + 180., 360.) - 180.) diff --git a/mpas_analysis/ocean/woce_transects.py b/mpas_analysis/ocean/woce_transects.py index 2e3a68de7..f7f3ad9e4 100644 --- a/mpas_analysis/ocean/woce_transects.py +++ b/mpas_analysis/ocean/woce_transects.py @@ -35,14 +35,14 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted as a transect - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors @@ -70,7 +70,7 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): verticalComparisonGrid = None else: verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', use_numpyfunc=True) + sectionName, 'verticalComparisonGrid', allow_numpy=True) verticalBounds = config.getexpression(sectionName, 'verticalBounds') diff --git a/mpas_analysis/sea_ice/climatology_map_albedo.py b/mpas_analysis/sea_ice/climatology_map_albedo.py index 35c00e59e..3d313d8f5 100755 --- a/mpas_analysis/sea_ice/climatology_map_albedo.py +++ b/mpas_analysis/sea_ice/climatology_map_albedo.py @@ -34,7 +34,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -43,7 +43,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_area_pond.py b/mpas_analysis/sea_ice/climatology_map_area_pond.py index 595bb3a6c..06cc8ba65 100755 --- a/mpas_analysis/sea_ice/climatology_map_area_pond.py +++ b/mpas_analysis/sea_ice/climatology_map_area_pond.py @@ -34,7 +34,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -43,7 +43,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_area_ridge.py b/mpas_analysis/sea_ice/climatology_map_area_ridge.py index 11c79c138..331f097a5 100755 --- a/mpas_analysis/sea_ice/climatology_map_area_ridge.py +++ b/mpas_analysis/sea_ice/climatology_map_area_ridge.py @@ -34,7 +34,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -43,7 +43,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_berg_conc.py b/mpas_analysis/sea_ice/climatology_map_berg_conc.py index 942c5d339..a2d97772c 100644 --- a/mpas_analysis/sea_ice/climatology_map_berg_conc.py +++ b/mpas_analysis/sea_ice/climatology_map_berg_conc.py @@ -35,7 +35,7 @@ def __init__(self, config, mpasClimatologyTask, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` @@ -44,7 +44,7 @@ def __init__(self, config, mpasClimatologyTask, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_melting.py b/mpas_analysis/sea_ice/climatology_map_melting.py index 129249e7a..713156df6 100755 --- a/mpas_analysis/sea_ice/climatology_map_melting.py +++ b/mpas_analysis/sea_ice/climatology_map_melting.py @@ -35,7 +35,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -44,7 +44,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_production.py b/mpas_analysis/sea_ice/climatology_map_production.py index 043332437..4bbea54af 100755 --- a/mpas_analysis/sea_ice/climatology_map_production.py +++ b/mpas_analysis/sea_ice/climatology_map_production.py @@ -35,7 +35,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -44,7 +44,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py b/mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py index f74ad713f..9162638eb 100644 --- a/mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py +++ b/mpas_analysis/sea_ice/climatology_map_sea_ice_conc.py @@ -38,7 +38,7 @@ def __init__(self, config, mpasClimatologyTask, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` @@ -47,7 +47,7 @@ def __init__(self, config, mpasClimatologyTask, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py b/mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py index f165d3f15..9f033ec7f 100644 --- a/mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py +++ b/mpas_analysis/sea_ice/climatology_map_sea_ice_thick.py @@ -38,7 +38,7 @@ def __init__(self, config, mpasClimatologyTask, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasClimatologyTask : ``MpasClimatologyTask`` @@ -47,7 +47,7 @@ def __init__(self, config, mpasClimatologyTask, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - controlConfig : mpas_tools.config.MpasConfigParser, optional + controlConfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_snow_depth.py b/mpas_analysis/sea_ice/climatology_map_snow_depth.py index a8bd7c732..1d08f75a5 100755 --- a/mpas_analysis/sea_ice/climatology_map_snow_depth.py +++ b/mpas_analysis/sea_ice/climatology_map_snow_depth.py @@ -34,7 +34,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -43,7 +43,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_snowice.py b/mpas_analysis/sea_ice/climatology_map_snowice.py index 08c0e833f..c5b3888be 100755 --- a/mpas_analysis/sea_ice/climatology_map_snowice.py +++ b/mpas_analysis/sea_ice/climatology_map_snowice.py @@ -34,7 +34,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -43,7 +43,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_snowmelt.py b/mpas_analysis/sea_ice/climatology_map_snowmelt.py index 0aefb3008..6942c3b9f 100755 --- a/mpas_analysis/sea_ice/climatology_map_snowmelt.py +++ b/mpas_analysis/sea_ice/climatology_map_snowmelt.py @@ -34,7 +34,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -43,7 +43,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_tendency_area_thermo.py b/mpas_analysis/sea_ice/climatology_map_tendency_area_thermo.py index 968767e3e..03f34a7ed 100755 --- a/mpas_analysis/sea_ice/climatology_map_tendency_area_thermo.py +++ b/mpas_analysis/sea_ice/climatology_map_tendency_area_thermo.py @@ -34,7 +34,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -43,7 +43,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_tendency_area_transp.py b/mpas_analysis/sea_ice/climatology_map_tendency_area_transp.py index e032f0dc4..58d17a4c4 100755 --- a/mpas_analysis/sea_ice/climatology_map_tendency_area_transp.py +++ b/mpas_analysis/sea_ice/climatology_map_tendency_area_transp.py @@ -34,7 +34,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -43,7 +43,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_tendency_volume_thermo.py b/mpas_analysis/sea_ice/climatology_map_tendency_volume_thermo.py index 7994604f1..b9437b6dc 100755 --- a/mpas_analysis/sea_ice/climatology_map_tendency_volume_thermo.py +++ b/mpas_analysis/sea_ice/climatology_map_tendency_volume_thermo.py @@ -34,7 +34,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -43,7 +43,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_tendency_volume_transp.py b/mpas_analysis/sea_ice/climatology_map_tendency_volume_transp.py index 5b4e17a7f..4919d7538 100755 --- a/mpas_analysis/sea_ice/climatology_map_tendency_volume_transp.py +++ b/mpas_analysis/sea_ice/climatology_map_tendency_volume_transp.py @@ -34,7 +34,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -43,7 +43,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/climatology_map_volume_ridge.py b/mpas_analysis/sea_ice/climatology_map_volume_ridge.py index 14c98ac03..3e85cb904 100755 --- a/mpas_analysis/sea_ice/climatology_map_volume_ridge.py +++ b/mpas_analysis/sea_ice/climatology_map_volume_ridge.py @@ -34,7 +34,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpas_climatology_task : mpas_analysis.shared.climatology.MpasClimatologyTask @@ -43,7 +43,7 @@ def __init__(self, config, mpas_climatology_task, hemisphere, hemisphere : {'NH', 'SH'} The hemisphere to plot - control_config : mpas_tools.config.MpasConfigParser, optional + control_config : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/sea_ice/time_series.py b/mpas_analysis/sea_ice/time_series.py index 0c4f3b08d..b0e0fcca0 100644 --- a/mpas_analysis/sea_ice/time_series.py +++ b/mpas_analysis/sea_ice/time_series.py @@ -41,7 +41,7 @@ class TimeSeriesSeaIce(AnalysisTask): mpasTimeSeriesTask : ``MpasTimeSeriesTask`` The task that extracts the time series from MPAS monthly output - controlconfig : mpas_tools.config.MpasConfigParser + controlconfig : tranche.Tranche Configuration options for a control run (if any) """ @@ -56,13 +56,13 @@ def __init__(self, config, mpasTimeSeriesTask, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options mpasTimeSeriesTask : ``MpasTimeSeriesTask`` The task that extracts the time series from MPAS monthly output - controlconfig : mpas_tools.config.MpasConfigParser, optional + controlconfig : tranche.Tranche, optional Configuration options for a control run (if any) """ # Authors diff --git a/mpas_analysis/shared/analysis_task.py b/mpas_analysis/shared/analysis_task.py index 7a5a4d8c7..f47660658 100644 --- a/mpas_analysis/shared/analysis_task.py +++ b/mpas_analysis/shared/analysis_task.py @@ -33,7 +33,7 @@ class AnalysisTask(Process): Attributes ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options taskName : str @@ -110,7 +110,7 @@ def __init__(self, config, taskName, componentName, tags=[], Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options taskName : str diff --git a/mpas_analysis/shared/climatology/climatology.py b/mpas_analysis/shared/climatology/climatology.py index 82305e11c..6436f7d80 100644 --- a/mpas_analysis/shared/climatology/climatology.py +++ b/mpas_analysis/shared/climatology/climatology.py @@ -47,7 +47,7 @@ def get_remapper(config, sourceDescriptor, comparisonDescriptor, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options sourceDescriptor : pyremap.MeshDescriptor @@ -334,7 +334,7 @@ def remap_and_write_climatology(config, climatologyDataSet, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options climatologyDataSet : ``xarray.DataSet`` or ``xarray.DataArray`` object @@ -406,7 +406,7 @@ def get_unmasked_mpas_climatology_directory(config, op='avg'): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche configuration options op : {'avg', 'min', 'max'} @@ -434,7 +434,7 @@ def get_unmasked_mpas_climatology_file_name(config, season, componentName, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche configuration options season : str @@ -486,7 +486,7 @@ def get_masked_mpas_climatology_file_name(config, season, componentName, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options season : str @@ -553,7 +553,7 @@ def get_remapped_mpas_climatology_file_name(config, season, componentName, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options season : str diff --git a/mpas_analysis/shared/climatology/comparison_descriptors.py b/mpas_analysis/shared/climatology/comparison_descriptors.py index 92a0b6a21..25ccd099a 100644 --- a/mpas_analysis/shared/climatology/comparison_descriptors.py +++ b/mpas_analysis/shared/climatology/comparison_descriptors.py @@ -34,7 +34,7 @@ def get_comparison_descriptor(config, comparison_grid_name): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options comparison_grid_name : {'latlon', 'antarctic', 'arctic', 'north_atlantic', @@ -71,7 +71,7 @@ def _get_lat_lon_comparison_descriptor(config): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options Returns @@ -105,7 +105,7 @@ def _get_projection_comparison_descriptor(config, comparison_grid_name): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options comparison_grid_name : str diff --git a/mpas_analysis/shared/climatology/mpas_climatology_task.py b/mpas_analysis/shared/climatology/mpas_climatology_task.py index 921f0da56..c0414a287 100644 --- a/mpas_analysis/shared/climatology/mpas_climatology_task.py +++ b/mpas_analysis/shared/climatology/mpas_climatology_task.py @@ -86,7 +86,7 @@ def __init__(self, config, componentName, taskName=None, op='avg'): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options componentName : {'ocean', 'seaIce'} diff --git a/mpas_analysis/shared/climatology/ref_year_mpas_climatology_task.py b/mpas_analysis/shared/climatology/ref_year_mpas_climatology_task.py index e186734a8..989d5257c 100644 --- a/mpas_analysis/shared/climatology/ref_year_mpas_climatology_task.py +++ b/mpas_analysis/shared/climatology/ref_year_mpas_climatology_task.py @@ -11,7 +11,7 @@ from io import StringIO -from mpas_tools.config import MpasConfigParser +from tranche import Tranche from mpas_analysis.shared.climatology import MpasClimatologyTask from mpas_analysis.shared.timekeeping.utility import get_simulation_start_time @@ -39,7 +39,7 @@ def __init__(self, config, componentName, taskName=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options componentName : {'ocean', 'seaIce'} diff --git a/mpas_analysis/shared/generalized_reader/generalized_reader.py b/mpas_analysis/shared/generalized_reader/generalized_reader.py index 6c47c929a..ae0ece8f9 100644 --- a/mpas_analysis/shared/generalized_reader/generalized_reader.py +++ b/mpas_analysis/shared/generalized_reader/generalized_reader.py @@ -50,7 +50,7 @@ def open_multifile_dataset(fileNames, calendar, config, calendar : {``'gregorian'``, ``'noleap'``}, optional The name of one of the calendars supported by MPAS cores - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options simulationStartTime : string, optional diff --git a/mpas_analysis/shared/html/image_xml.py b/mpas_analysis/shared/html/image_xml.py index 71340c807..6fe894efa 100644 --- a/mpas_analysis/shared/html/image_xml.py +++ b/mpas_analysis/shared/html/image_xml.py @@ -33,7 +33,7 @@ def write_image_xml(config, filePrefix, componentName, componentSubdirectory, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche contains config options filePrefix : str diff --git a/mpas_analysis/shared/html/pages.py b/mpas_analysis/shared/html/pages.py index 302ce7dd0..2a9097e0f 100644 --- a/mpas_analysis/shared/html/pages.py +++ b/mpas_analysis/shared/html/pages.py @@ -29,7 +29,7 @@ def generate_html(config, analyses, controlConfig, customConfigFiles): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Config options analysis : ``OrderedDict`` of ``AnalysisTask`` objects @@ -38,7 +38,7 @@ def generate_html(config, analyses, controlConfig, customConfigFiles): the list of files to include on the webpage for the associated component. - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche Config options for a control run customConfigFiles : list of str @@ -108,10 +108,10 @@ class MainPage(object): Attributes ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Config options - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche Config options for a control run customConfigFiles : list of str @@ -136,10 +136,10 @@ def __init__(self, config, controlConfig, customConfigFiles): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Config options - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche Config options for a control run customConfigFiles : list of str @@ -302,10 +302,10 @@ class ComponentPage(object): Attributes ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Config options - controlConfig : mpas_tools.config.MpasConfigParser + controlConfig : tranche.Tranche Config options for a control run name : str @@ -334,7 +334,7 @@ def __init__(self, config, name, subdirectory, controlConfig=None): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Config options name : str @@ -345,7 +345,7 @@ def __init__(self, config, name, subdirectory, controlConfig=None): subdirecory : str The subdirectory for the component's webpage - controlConfig : mpas_tools.config.MpasConfigParser, optional + controlConfig : tranche.Tranche, optional Config options for a control run """ # Authors @@ -387,7 +387,7 @@ def add_image(xmlFileName, config, components, controlConfig=None): xmlFileName : str The full path to the XML file describing the image to be added - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche contains config options components : OrederdDict of dict @@ -396,7 +396,7 @@ def add_image(xmlFileName, config, components, controlConfig=None): be added. ``components`` should be viewed as an input and output parameter, since it is modified by this function. - controlConfig : mpas_tools.config.MpasConfigParser, optional + controlConfig : tranche.Tranche, optional Config options for a control run """ # Authors diff --git a/mpas_analysis/shared/io/utility.py b/mpas_analysis/shared/io/utility.py index 9c99c0f49..a5e3a98b7 100644 --- a/mpas_analysis/shared/io/utility.py +++ b/mpas_analysis/shared/io/utility.py @@ -112,7 +112,7 @@ def build_config_full_path(config, section, relativePathOption, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche configuration from which to read the path section : str @@ -163,7 +163,7 @@ def get_region_mask(config, regionMaskFile): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche configuration from which to read the path regionMaskFile : str @@ -223,7 +223,7 @@ def build_obs_path(config, component, relativePathOption=None, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche configuration from which to read the path component : {'ocean', 'seaIce', 'iceberg'} diff --git a/mpas_analysis/shared/plot/climatology_map.py b/mpas_analysis/shared/plot/climatology_map.py index 02dbd9949..53c714197 100644 --- a/mpas_analysis/shared/plot/climatology_map.py +++ b/mpas_analysis/shared/plot/climatology_map.py @@ -61,7 +61,7 @@ def plot_polar_comparison( Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche the configuration, containing a [plot] section with options that control plotting @@ -277,7 +277,7 @@ def plot_global_comparison( Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche the configuration, containing a [plot] section with options that control plotting @@ -475,7 +475,7 @@ def plot_projection_comparison( Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche the configuration, containing a [plot] section with options that control plotting @@ -663,8 +663,8 @@ def _plot_panel(ax, title, array, colormap, norm, levels, ticks, contours, else: figsize = config.getexpression(section, 'threePanelHorizFigSize') subplots = [131, 132, 133] - latLines = config.getexpression(section, 'latLines', use_numpyfunc=True) - lonLines = config.getexpression(section, 'lonLines', use_numpyfunc=True) + latLines = config.getexpression(section, 'latLines', allow_numpy=True) + lonLines = config.getexpression(section, 'lonLines', allow_numpy=True) # put latitude labels on the left unless we're in a polar projection left_labels = projectionName not in ['arctic', 'antarctic'] diff --git a/mpas_analysis/shared/plot/colormap.py b/mpas_analysis/shared/plot/colormap.py index c20e4010a..abfa8357d 100644 --- a/mpas_analysis/shared/plot/colormap.py +++ b/mpas_analysis/shared/plot/colormap.py @@ -103,7 +103,7 @@ def setup_colormap(config, configSectionName, suffix=''): if config.has_option(configSectionName, option): contours = config.getexpression(configSectionName, option, - use_numpyfunc=True) + allow_numpy=True) if isinstance(contours, str) and contours == 'none': contours = None @@ -390,7 +390,7 @@ def _setup_colormap_and_norm(config, configSectionName, suffix=''): try: ticks = config.getexpression( configSectionName, f'colorbarTicks{suffix}', - use_numpyfunc=True) + allow_numpy=True) except configparser.NoOptionError: ticks = None @@ -433,12 +433,12 @@ def _setup_indexed_colormap(config, configSectionName, suffix=''): indices = config.getexpression(configSectionName, f'colormapIndices{suffix}', - use_numpyfunc=True) + allow_numpy=True) try: levels = config.getexpression( configSectionName, f'colorbarLevels{suffix}', - use_numpyfunc=True) + allow_numpy=True) except configparser.NoOptionError: levels = None @@ -465,7 +465,7 @@ def _setup_indexed_colormap(config, configSectionName, suffix=''): try: ticks = config.getexpression( configSectionName, f'colorbarTicks{suffix}', - use_numpyfunc=True) + allow_numpy=True) except configparser.NoOptionError: ticks = levels diff --git a/mpas_analysis/shared/plot/plot_climatology_map_subtask.py b/mpas_analysis/shared/plot/plot_climatology_map_subtask.py index 59d1b341b..f94c678a8 100644 --- a/mpas_analysis/shared/plot/plot_climatology_map_subtask.py +++ b/mpas_analysis/shared/plot/plot_climatology_map_subtask.py @@ -141,7 +141,7 @@ def __init__(self, parentTask, season, comparisonGridName, A second subtask for remapping another MPAS climatology to plot in the second panel and compare with in the third panel - controlConfig : mpas_tools.config.MpasConfigParser, optional + controlConfig : tranche.Tranche, optional Configuration options for a control run (if any) depth : {float, 'top', 'bot'}, optional diff --git a/mpas_analysis/shared/plot/save.py b/mpas_analysis/shared/plot/save.py index 2629397fd..5f6d73400 100644 --- a/mpas_analysis/shared/plot/save.py +++ b/mpas_analysis/shared/plot/save.py @@ -23,7 +23,7 @@ def savefig(filename, config, tight=True, pad_inches=0.1): filename : str the file name to be written - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options tight : bool, optional diff --git a/mpas_analysis/shared/regions/compute_region_masks.py b/mpas_analysis/shared/regions/compute_region_masks.py index 4128d9a37..a23e6448f 100644 --- a/mpas_analysis/shared/regions/compute_region_masks.py +++ b/mpas_analysis/shared/regions/compute_region_masks.py @@ -31,7 +31,7 @@ def __init__(self, config, conponentName): Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Configuration options conponentName : str diff --git a/mpas_analysis/shared/time_series/mpas_time_series_task.py b/mpas_analysis/shared/time_series/mpas_time_series_task.py index 9a8e448bb..8fedaad47 100644 --- a/mpas_analysis/shared/time_series/mpas_time_series_task.py +++ b/mpas_analysis/shared/time_series/mpas_time_series_task.py @@ -59,7 +59,7 @@ def __init__(self, config, componentName, taskName=None, Parameters ---------- - config : mpas_tools.config.MpasConfigParser + config : tranche.Tranche Contains configuration options componentName : {'ocean', 'seaIce'} diff --git a/mpas_analysis/test/test_analysis_task.py b/mpas_analysis/test/test_analysis_task.py index 2080248e9..4673b7df0 100644 --- a/mpas_analysis/test/test_analysis_task.py +++ b/mpas_analysis/test/test_analysis_task.py @@ -16,7 +16,7 @@ import pytest -from mpas_tools.config import MpasConfigParser +from tranche import Tranche from mpas_analysis.test import TestCase from mpas_analysis.shared.analysis_task import AnalysisTask @@ -27,7 +27,7 @@ class TestAnalysisTask(TestCase): def test_checkGenerate(self): def doTest(generate, expectedResults): - config = MpasConfigParser() + config = Tranche() config.set('output', 'generate', generate) for taskName in expectedResults: genericTask = AnalysisTask(config=config, diff --git a/mpas_analysis/test/test_climatology.py b/mpas_analysis/test/test_climatology.py index 0d9c79168..aa6d88486 100644 --- a/mpas_analysis/test/test_climatology.py +++ b/mpas_analysis/test/test_climatology.py @@ -23,7 +23,7 @@ import xarray from pyremap import MpasCellMeshDescriptor, LatLonGridDescriptor -from mpas_tools.config import MpasConfigParser +from tranche import Tranche from mpas_analysis.test import TestCase, loaddatadir from mpas_analysis.shared.generalized_reader.generalized_reader \ @@ -47,7 +47,7 @@ def tearDown(self): shutil.rmtree(self.test_dir) def setup_config(self, maxChunkSize=10000): - config = MpasConfigParser() + config = Tranche() config.set('execute', 'mapParallelExec', 'None') config.set('execute', 'mapMpiTasks', '1') diff --git a/mpas_analysis/test/test_generalized_reader.py b/mpas_analysis/test/test_generalized_reader.py index b5a35dc7a..1c80adbf9 100644 --- a/mpas_analysis/test/test_generalized_reader.py +++ b/mpas_analysis/test/test_generalized_reader.py @@ -18,7 +18,7 @@ import numpy import pytest -from mpas_tools.config import MpasConfigParser +from tranche import Tranche from mpas_analysis.test import TestCase, loaddatadir from mpas_analysis.shared.generalized_reader.generalized_reader \ @@ -29,7 +29,7 @@ class TestGeneralizedReader(TestCase): def setup_config(self, maxChunkSize=10000): - config = MpasConfigParser() + config = Tranche() config.set('input', 'maxChunkSize', str(maxChunkSize)) return config diff --git a/mpas_analysis/test/test_mpas_climatology_task.py b/mpas_analysis/test/test_mpas_climatology_task.py index d502666e8..d0e57d3f6 100644 --- a/mpas_analysis/test/test_mpas_climatology_task.py +++ b/mpas_analysis/test/test_mpas_climatology_task.py @@ -19,7 +19,7 @@ import shutil import os -from mpas_tools.config import MpasConfigParser +from tranche import Tranche from mpas_analysis.test import TestCase, loaddatadir from mpas_analysis.shared.climatology import MpasClimatologyTask, \ @@ -44,7 +44,7 @@ def tearDown(self): def setup_config(self): configPath = self.datadir.join('QU240.cfg') - config = MpasConfigParser() + config = Tranche() config.add_from_file(str(configPath)) config.set('input', 'baseDirectory', str(self.datadir)) config.set('output', 'baseDirectory', str(self.test_dir)) diff --git a/mpas_analysis/test/test_remap_obs_clim_subtask.py b/mpas_analysis/test/test_remap_obs_clim_subtask.py index 905b21ba8..505328763 100644 --- a/mpas_analysis/test/test_remap_obs_clim_subtask.py +++ b/mpas_analysis/test/test_remap_obs_clim_subtask.py @@ -21,7 +21,7 @@ import xarray from pyremap import LatLonGridDescriptor -from mpas_tools.config import MpasConfigParser +from tranche import Tranche from mpas_analysis.test import TestCase, loaddatadir from mpas_analysis.shared.climatology import RemapObservedClimatologySubtask @@ -104,7 +104,7 @@ def tearDown(self): def setup_config(self): configPath = self.datadir.join('remap_obs.cfg') - config = MpasConfigParser() + config = Tranche() config.add_from_file(str(configPath)) config.set('input', 'baseDirectory', str(self.datadir)) config.set('diagnostics', 'base_path', str(self.datadir)) diff --git a/pyproject.toml b/pyproject.toml index 779d74769..50b59412d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ dependencies = [ "requests", "scipy >=1.7.0", "shapely >=2.0,<3.0", + "tranche >=0.1.1", "xarray >=0.14.1" ] From c0cf2d45263666092c941686e3de723c7b7a9525 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 19 Sep 2025 07:06:32 -0500 Subject: [PATCH 108/116] Switch to getnumpy() config method. This is more convenient for expressions that may involve numpy. --- mpas_analysis/ocean/antship_transects.py | 4 ++-- mpas_analysis/ocean/climatology_map_bsf.py | 4 +--- .../ocean/climatology_map_ohc_anomaly.py | 6 +++--- .../ocean/geojson_netcdf_transects.py | 5 +++-- mpas_analysis/ocean/osnap_transects.py | 5 +++-- mpas_analysis/ocean/regional_ts_diagrams.py | 10 ++++------ mpas_analysis/ocean/sose_transects.py | 11 +++++----- mpas_analysis/ocean/woa_transects.py | 8 ++++---- mpas_analysis/ocean/woce_transects.py | 5 +++-- mpas_analysis/shared/plot/climatology_map.py | 4 ++-- mpas_analysis/shared/plot/colormap.py | 20 +++++-------------- 11 files changed, 35 insertions(+), 47 deletions(-) diff --git a/mpas_analysis/ocean/antship_transects.py b/mpas_analysis/ocean/antship_transects.py index 723a17cdf..eb7f519c5 100644 --- a/mpas_analysis/ocean/antship_transects.py +++ b/mpas_analysis/ocean/antship_transects.py @@ -68,8 +68,8 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): if verticalComparisonGridName in ['mpas', 'obs']: verticalComparisonGrid = None else: - verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', allow_numpy=True) + verticalComparisonGrid = config.getnumpy( + sectionName, 'verticalComparisonGrid') verticalBounds = config.getexpression(sectionName, 'verticalBounds') diff --git a/mpas_analysis/ocean/climatology_map_bsf.py b/mpas_analysis/ocean/climatology_map_bsf.py index 8cb812f05..4dac10212 100644 --- a/mpas_analysis/ocean/climatology_map_bsf.py +++ b/mpas_analysis/ocean/climatology_map_bsf.py @@ -62,9 +62,7 @@ def __init__(self, config, mpas_climatology_task, control_config=None): # read in what seasons we want to plot seasons = config.getexpression(section_name, 'seasons') - depth_ranges = config.getexpression(section_name, - 'depthRanges', - allow_numpy=True) + depth_ranges = config.getnumpy(section_name, 'depthRanges') if len(seasons) == 0: raise ValueError(f'config section {section_name} does not contain ' f'valid list of seasons') diff --git a/mpas_analysis/ocean/climatology_map_ohc_anomaly.py b/mpas_analysis/ocean/climatology_map_ohc_anomaly.py index 672abe530..e0a39b1c2 100644 --- a/mpas_analysis/ocean/climatology_map_ohc_anomaly.py +++ b/mpas_analysis/ocean/climatology_map_ohc_anomaly.py @@ -79,9 +79,9 @@ def __init__(self, config, mpas_climatology_task, raise ValueError(f'config section {section_name} does not contain ' f'valid list of comparison grids') - depth_ranges = config.getexpression('climatologyMapOHCAnomaly', - 'depthRanges', - allow_numpy=True) + depth_ranges = config.getnumpy( + 'climatologyMapOHCAnomaly', 'depthRanges' + ) mpas_field_name = 'deltaOHC' diff --git a/mpas_analysis/ocean/geojson_netcdf_transects.py b/mpas_analysis/ocean/geojson_netcdf_transects.py index 6683257f2..298c6f297 100644 --- a/mpas_analysis/ocean/geojson_netcdf_transects.py +++ b/mpas_analysis/ocean/geojson_netcdf_transects.py @@ -77,8 +77,9 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): if verticalComparisonGridName in ['mpas', 'obs']: verticalComparisonGrid = None else: - verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', allow_numpy=True) + verticalComparisonGrid = config.getnumpy( + sectionName, 'verticalComparisonGrid' + ) availableVariables = config.getexpression( sectionName, 'availableVariables') diff --git a/mpas_analysis/ocean/osnap_transects.py b/mpas_analysis/ocean/osnap_transects.py index e99c59001..e941e92fe 100644 --- a/mpas_analysis/ocean/osnap_transects.py +++ b/mpas_analysis/ocean/osnap_transects.py @@ -67,8 +67,9 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): if verticalComparisonGridName in ['mpas', 'obs']: verticalComparisonGrid = None else: - verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', allow_numpy=True) + verticalComparisonGrid = config.getnumpy( + sectionName, 'verticalComparisonGrid' + ) verticalBounds = config.getexpression(sectionName, 'verticalBounds') diff --git a/mpas_analysis/ocean/regional_ts_diagrams.py b/mpas_analysis/ocean/regional_ts_diagrams.py index f808075a4..151cd55a6 100644 --- a/mpas_analysis/ocean/regional_ts_diagrams.py +++ b/mpas_analysis/ocean/regional_ts_diagrams.py @@ -1010,8 +1010,8 @@ def run_task(self): plotFields.append({'S': obsS, 'T': obsT, 'z': obsZ, 'vol': obsVol, 'title': obsName}) - Tbins = config.getexpression(sectionName, 'Tbins', allow_numpy=True) - Sbins = config.getexpression(sectionName, 'Sbins', allow_numpy=True) + Tbins = config.getnumpy(sectionName, 'Tbins') + Sbins = config.getnumpy(sectionName, 'Sbins') normType = config.get(sectionName, 'normType') @@ -1206,10 +1206,8 @@ def _plot_volumetric_panel(self, T, S, volume): config = self.config sectionName = self.sectionName cmap = config.get(sectionName, 'colorMap') - Tbins = config.getexpression(sectionName, 'Tbins', - allow_numpy=True) - Sbins = config.getexpression(sectionName, 'Sbins', - allow_numpy=True) + Tbins = config.getnumpy(sectionName, 'Tbins') + Sbins = config.getnumpy(sectionName, 'Sbins') hist, _, _, panel = plt.hist2d(S, T, bins=[Sbins, Tbins], weights=volume, cmap=cmap, zorder=1, diff --git a/mpas_analysis/ocean/sose_transects.py b/mpas_analysis/ocean/sose_transects.py index e49fc30df..d9f2bee57 100644 --- a/mpas_analysis/ocean/sose_transects.py +++ b/mpas_analysis/ocean/sose_transects.py @@ -79,13 +79,13 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): if verticalComparisonGridName in ['mpas', 'obs']: verticalComparisonGrid = None else: - verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', allow_numpy=True) + verticalComparisonGrid = config.getnumpy( + sectionName, 'verticalComparisonGrid' + ) verticalBounds = config.getexpression(sectionName, 'verticalBounds') - longitudes = sorted(config.getexpression(sectionName, 'longitudes', - allow_numpy=True)) + longitudes = sorted(config.getnumpy(sectionName, 'longitudes')) fields = \ [{'prefix': 'temperature', @@ -295,8 +295,7 @@ def combine_observations(self): config = self.config - longitudes = sorted(config.getexpression('soseTransects', 'longitudes', - allow_numpy=True)) + longitudes = sorted(config.getnumpy('soseTransects', 'longitudes')) observationsDirectory = build_obs_path( config, 'ocean', 'soseSubdirectory') diff --git a/mpas_analysis/ocean/woa_transects.py b/mpas_analysis/ocean/woa_transects.py index f551b5e15..d48e8fd97 100644 --- a/mpas_analysis/ocean/woa_transects.py +++ b/mpas_analysis/ocean/woa_transects.py @@ -79,8 +79,9 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): if verticalComparisonGridName in ['mpas', 'obs']: verticalComparisonGrid = None else: - verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', allow_numpy=True) + verticalComparisonGrid = config.getnumpy( + sectionName, 'verticalComparisonGrid' + ) verticalBounds = config.getexpression(sectionName, 'verticalBounds') @@ -362,8 +363,7 @@ def build_observational_dataset(self, fileName, transectName): def _get_longitudes(config): - longitudes = config.getexpression('woaTransects', 'longitudes', - allow_numpy=True) + longitudes = config.getnumpy('woaTransects', 'longitudes') longitudes = np.array(longitudes) # make sure longitudes are between -180 and 180 longitudes = np.sort(np.mod(longitudes + 180., 360.) - 180.) diff --git a/mpas_analysis/ocean/woce_transects.py b/mpas_analysis/ocean/woce_transects.py index f7f3ad9e4..039569d80 100644 --- a/mpas_analysis/ocean/woce_transects.py +++ b/mpas_analysis/ocean/woce_transects.py @@ -69,8 +69,9 @@ def __init__(self, config, mpasClimatologyTask, controlConfig=None): if verticalComparisonGridName in ['mpas', 'obs']: verticalComparisonGrid = None else: - verticalComparisonGrid = config.getexpression( - sectionName, 'verticalComparisonGrid', allow_numpy=True) + verticalComparisonGrid = config.getnumpy( + sectionName, 'verticalComparisonGrid' + ) verticalBounds = config.getexpression(sectionName, 'verticalBounds') diff --git a/mpas_analysis/shared/plot/climatology_map.py b/mpas_analysis/shared/plot/climatology_map.py index 53c714197..0eb1e6092 100644 --- a/mpas_analysis/shared/plot/climatology_map.py +++ b/mpas_analysis/shared/plot/climatology_map.py @@ -663,8 +663,8 @@ def _plot_panel(ax, title, array, colormap, norm, levels, ticks, contours, else: figsize = config.getexpression(section, 'threePanelHorizFigSize') subplots = [131, 132, 133] - latLines = config.getexpression(section, 'latLines', allow_numpy=True) - lonLines = config.getexpression(section, 'lonLines', allow_numpy=True) + latLines = config.getnumpy(section, 'latLines') + lonLines = config.getnumpy(section, 'lonLines') # put latitude labels on the left unless we're in a polar projection left_labels = projectionName not in ['arctic', 'antarctic'] diff --git a/mpas_analysis/shared/plot/colormap.py b/mpas_analysis/shared/plot/colormap.py index abfa8357d..1c2cf7238 100644 --- a/mpas_analysis/shared/plot/colormap.py +++ b/mpas_analysis/shared/plot/colormap.py @@ -101,9 +101,7 @@ def setup_colormap(config, configSectionName, suffix=''): option = f'contourLevels{suffix}' if config.has_option(configSectionName, option): - contours = config.getexpression(configSectionName, - option, - allow_numpy=True) + contours = config.getnumpy(configSectionName, option) if isinstance(contours, str) and contours == 'none': contours = None @@ -388,9 +386,7 @@ def _setup_colormap_and_norm(config, configSectionName, suffix=''): f'{configSectionName}') try: - ticks = config.getexpression( - configSectionName, f'colorbarTicks{suffix}', - allow_numpy=True) + ticks = config.getnumpy(configSectionName, f'colorbarTicks{suffix}') except configparser.NoOptionError: ticks = None @@ -431,14 +427,10 @@ def _setup_indexed_colormap(config, configSectionName, suffix=''): colormap = plt.get_cmap(config.get(configSectionName, f'colormapName{suffix}')) - indices = config.getexpression(configSectionName, - f'colormapIndices{suffix}', - allow_numpy=True) + indices = config.getnumpy(configSectionName, f'colormapIndices{suffix}') try: - levels = config.getexpression( - configSectionName, f'colorbarLevels{suffix}', - allow_numpy=True) + levels = config.getnumpy(configSectionName, f'colorbarLevels{suffix}') except configparser.NoOptionError: levels = None @@ -463,9 +455,7 @@ def _setup_indexed_colormap(config, configSectionName, suffix=''): norm = cols.BoundaryNorm(levels, colormap.N) try: - ticks = config.getexpression( - configSectionName, f'colorbarTicks{suffix}', - allow_numpy=True) + ticks = config.getnumpy(configSectionName, f'colorbarTicks{suffix}') except configparser.NoOptionError: ticks = levels From ab39edd3f9b89deb7c20c0cc97d02ae222b89f7a Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 19 Sep 2025 07:07:12 -0500 Subject: [PATCH 109/116] Update tutorial. --- docs/tutorials/dev_understand_a_task.rst | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/dev_understand_a_task.rst b/docs/tutorials/dev_understand_a_task.rst index 154a0ee2d..46a123a5a 100644 --- a/docs/tutorials/dev_understand_a_task.rst +++ b/docs/tutorials/dev_understand_a_task.rst @@ -301,9 +301,8 @@ find something unexpected: raise ValueError(f'config section {section_name} does not contain ' f'valid list of comparison grids') - depth_ranges = config.getexpression('climatologyMapOHCAnomaly', - 'depthRanges', - allow_numpy=True) + depth_ranges = config.getnumpy('climatologyMapOHCAnomaly', + 'depthRanges') By default, these config options look like this: @@ -944,9 +943,8 @@ here is the full analysis task as described in this tutorial: raise ValueError(f'config section {section_name} does not contain ' f'valid list of comparison grids') - depth_ranges = config.getexpression('climatologyMapOHCAnomaly', - 'depthRanges', - allow_numpy=True) + depth_ranges = config.getnumpy('climatologyMapOHCAnomaly', + 'depthRanges') mpas_field_name = 'deltaOHC' From 861580ded7d9b72f50644b81a6c696f599484384 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 19 Sep 2025 07:15:12 -0500 Subject: [PATCH 110/116] Ignore autogenerated quick_start.rst --- docs/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/.gitignore b/docs/.gitignore index 9d852aad7..8fd093281 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -14,4 +14,4 @@ design_docs/remapper.rst design_docs/template.rst design_docs/timekeeping_reorg.rst design_docs/variable_mapping_reorg.rst -/quick_start.rst +users_guide/quick_start.rst From e21b5ebc0570def32a0659602079036e3854da1a Mon Sep 17 00:00:00 2001 From: Irena Vankova Date: Mon, 22 Sep 2025 17:17:03 -0600 Subject: [PATCH 111/116] change to openOceanMask --- mpas_analysis/ocean/ocean_regional_profiles.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mpas_analysis/ocean/ocean_regional_profiles.py b/mpas_analysis/ocean/ocean_regional_profiles.py index b5be6444e..d8e021041 100644 --- a/mpas_analysis/ocean/ocean_regional_profiles.py +++ b/mpas_analysis/ocean/ocean_regional_profiles.py @@ -349,9 +349,7 @@ def run_task(self): dsMesh = xr.open_dataset(meshFilename) dsMesh = dsMesh.isel(Time=0) areaCell = dsMesh.areaCell - landIceFraction = dsMesh.landIceFraction - landIceFraction = xr.where(landIceFraction > 0, 1, landIceFraction) - landIceFraction = -1*(landIceFraction-1) + openOceanMask = xr.where(dsMesh.landIceMask > 0, 0, 1) nVertLevels = dsMesh.sizes['nVertLevels'] @@ -411,7 +409,7 @@ def run_task(self): self.logger.info(' {}'.format(field['titleName'])) var_mpas = dsLocal[variableName] - var_mpas_masked = var_mpas*landIceFraction + var_mpas_masked = var_mpas * openOceanMask var = var_mpas_masked.where(vertDepthMask) meanName = '{}_mean'.format(prefix) From 84dd087a920382baacc6694ec40ea888760548ee Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 19 Sep 2025 07:17:01 -0500 Subject: [PATCH 112/116] Update tranche lower bound --- ci/recipe/meta.yaml | 2 +- dev-spec.txt | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/recipe/meta.yaml b/ci/recipe/meta.yaml index 8db581370..f5079b449 100644 --- a/ci/recipe/meta.yaml +++ b/ci/recipe/meta.yaml @@ -48,7 +48,7 @@ requirements: - requests - scipy >=1.7.0 - shapely >=2.0,<3.0 - - tranche >=0.1.1 + - tranche >=0.2.3 - xarray >=0.14.1 test: diff --git a/dev-spec.txt b/dev-spec.txt index d29dbef05..56b63b9bf 100644 --- a/dev-spec.txt +++ b/dev-spec.txt @@ -28,7 +28,7 @@ python-dateutil requests scipy >=1.7.0 shapely >=2.0,<3.0 -tranche >=0.1.1 +tranche >=0.2.3 xarray >=0.14.1 # Development diff --git a/pyproject.toml b/pyproject.toml index 50b59412d..cd7d33ae8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ dependencies = [ "requests", "scipy >=1.7.0", "shapely >=2.0,<3.0", - "tranche >=0.1.1", + "tranche >=0.2.3", "xarray >=0.14.1" ] From 45d447f9f4c091175e6665599b395209c7ffc2cb Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Wed, 1 Oct 2025 06:25:26 -0500 Subject: [PATCH 113/116] Use open ocean mask in _masked_area_sum() method This ensures that both the area-weighted field (numerator) and the total area (denominator) include the same masking. --- .../ocean/ocean_regional_profiles.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/mpas_analysis/ocean/ocean_regional_profiles.py b/mpas_analysis/ocean/ocean_regional_profiles.py index d8e021041..5ff8f9485 100644 --- a/mpas_analysis/ocean/ocean_regional_profiles.py +++ b/mpas_analysis/ocean/ocean_regional_profiles.py @@ -383,7 +383,9 @@ def run_task(self): cellMasks = dsRegionMask.regionCellMasks regionNamesVar = dsRegionMask.regionNames - totalArea = self._masked_area_sum(cellMasks, areaCell, vertDepthMask) + totalArea = self._masked_area_sum( + cellMasks, openOceanMask, areaCell, vertDepthMask + ) datasets = [] for timeIndex, fileName in enumerate(inputFiles): @@ -408,18 +410,21 @@ def run_task(self): prefix = field['prefix'] self.logger.info(' {}'.format(field['titleName'])) - var_mpas = dsLocal[variableName] - var_mpas_masked = var_mpas * openOceanMask - var = var_mpas_masked.where(vertDepthMask) + var = dsLocal[variableName].where(vertDepthMask) meanName = '{}_mean'.format(prefix) - dsLocal[meanName] = \ - self._masked_area_sum(cellMasks, areaCell, var) / totalArea + dsLocal[meanName] = ( + self._masked_area_sum( + cellMasks, openOceanMask, areaCell, var + ) / totalArea + ) meanSquaredName = '{}_meanSquared'.format(prefix) - dsLocal[meanSquaredName] = \ - self._masked_area_sum(cellMasks, areaCell, var**2) / \ - totalArea + dsLocal[meanSquaredName] = ( + self._masked_area_sum( + cellMasks, openOceanMask, areaCell, var**2 + ) / totalArea + ) # drop the original variables dsLocal = dsLocal.drop_vars(variableList) @@ -452,12 +457,15 @@ def run_task(self): write_netcdf_with_fill(dsOut, outputFileName) @staticmethod - def _masked_area_sum(cellMasks, areaCell, var): + def _masked_area_sum(cellMasks, openOceanMask, areaCell, var): """sum a variable over the masked areas""" nRegions = cellMasks.sizes['nRegions'] totals = [] for index in range(nRegions): - mask = cellMasks.isel(nRegions=slice(index, index+1)) + mask = ( + cellMasks.isel(nRegions=slice(index, index+1)) * + openOceanMask + ) totals.append((mask * areaCell * var).sum('nCells')) total = xr.concat(totals, 'nRegions') From 53ebe5a858bf232452001619787a6e99b05610df Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Wed, 1 Oct 2025 15:27:35 -0500 Subject: [PATCH 114/116] Remove @altheaden from reviewer lists --- .github/dependabot.yml | 2 -- ci/recipe/meta.yaml | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f12baee6f..86db5f5e5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,9 +8,7 @@ updates: interval: "weekly" assignees: - "xylar" - - "altheaden" reviewers: - "xylar" - - "altheaden" - "andrewdnolan" diff --git a/ci/recipe/meta.yaml b/ci/recipe/meta.yaml index f5079b449..3e5bb97bf 100644 --- a/ci/recipe/meta.yaml +++ b/ci/recipe/meta.yaml @@ -83,6 +83,5 @@ about: extra: recipe-maintainers: - andrewdnolan - - altheaden - xylar - - jhkennedy \ No newline at end of file + - jhkennedy From 381013dd1a2336f072fa67e9cf66676db734b643 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 16 Oct 2025 05:49:47 -0500 Subject: [PATCH 115/116] Update unified suite to use python 3.13 This is just in the name of the `main` run and has no actual bearing on the python version used to deploy Unified. But for clarity, it would be better if it is correct. --- suite/run_e3sm_unified_suite.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suite/run_e3sm_unified_suite.bash b/suite/run_e3sm_unified_suite.bash index 994bc43d9..b060ede13 100755 --- a/suite/run_e3sm_unified_suite.bash +++ b/suite/run_e3sm_unified_suite.bash @@ -6,7 +6,7 @@ set -e branch=test_e3sm_unified # test building the docs -py=3.10 +py=3.13 machine=${E3SMU_MACHINE} ./suite/setup.py -p ${py} -r main_py${py} -b ${branch} --clean From cc8bab38633ded1f8340bad5ef818eeae893fdda Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sun, 9 Nov 2025 19:20:43 +0100 Subject: [PATCH 116/116] Update to v1.14.0 --- ci/recipe/meta.yaml | 2 +- mpas_analysis/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/recipe/meta.yaml b/ci/recipe/meta.yaml index 3e5bb97bf..10009615a 100644 --- a/ci/recipe/meta.yaml +++ b/ci/recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "MPAS-Analysis" %} -{% set version = "1.13.0" %} +{% set version = "1.14.0" %} {% set python_min = "3.10" %} package: diff --git a/mpas_analysis/version.py b/mpas_analysis/version.py index 9183bc661..ddbb9f041 100644 --- a/mpas_analysis/version.py +++ b/mpas_analysis/version.py @@ -1,2 +1,2 @@ -__version_info__ = (1, 13, 0) +__version_info__ = (1, 14, 0) __version__ = '.'.join(str(vi) for vi in __version_info__)