From 772695090054eec227c73ab473742439a604f916 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 22 Sep 2022 15:17:16 -0500 Subject: [PATCH 01/26] Prototype histograms with ssh climatology --- mpas_analysis/__main__.py | 2 + mpas_analysis/default.cfg | 1 + mpas_analysis/ocean/__init__.py | 1 + mpas_analysis/ocean/histogram_ssh.py | 272 +++++++++++++++++++++++++ mpas_analysis/shared/plot/__init__.py | 2 + mpas_analysis/shared/plot/histogram.py | 176 ++++++++++++++++ 6 files changed, 454 insertions(+) create mode 100644 mpas_analysis/ocean/histogram_ssh.py create mode 100644 mpas_analysis/shared/plot/histogram.py diff --git a/mpas_analysis/__main__.py b/mpas_analysis/__main__.py index 0d6891590..154a298b3 100755 --- a/mpas_analysis/__main__.py +++ b/mpas_analysis/__main__.py @@ -197,6 +197,8 @@ def build_analysis_list(config, controlConfig): controlConfig)) analyses.append(ocean.TimeSeriesTransport(config, controlConfig)) + analyses.append(ocean.HistogramSSH(config, oceanClimatolgyTasks['avg'], oceanRegionMasksTask, + controlConfig)) analyses.append(ocean.MeridionalHeatTransport( config, oceanClimatologyTasks['avg'], controlConfig)) diff --git a/mpas_analysis/default.cfg b/mpas_analysis/default.cfg index 435323dc1..829276e5f 100755 --- a/mpas_analysis/default.cfg +++ b/mpas_analysis/default.cfg @@ -171,6 +171,7 @@ logsSubdirectory = logs mpasClimatologySubdirectory = clim/mpas mappingSubdirectory = mapping timeSeriesSubdirectory = timeseries +histogramSubdirectory = histograms profilesSubdirectory = profiles maskSubdirectory = masks # provide an absolute path to put HTML in an alternative location (e.g. a web diff --git a/mpas_analysis/ocean/__init__.py b/mpas_analysis/ocean/__init__.py index 1315517b9..df5a0e50a 100644 --- a/mpas_analysis/ocean/__init__.py +++ b/mpas_analysis/ocean/__init__.py @@ -31,6 +31,7 @@ from mpas_analysis.ocean.time_series_ssh_anomaly import TimeSeriesSSHAnomaly from mpas_analysis.ocean.time_series_sst import TimeSeriesSST +from mpas_analysis.ocean.histogram_ssh import HistogramSSH from mpas_analysis.ocean.index_nino34 import IndexNino34 from mpas_analysis.ocean.streamfunction_moc import StreamfunctionMOC from mpas_analysis.ocean.meridional_heat_transport import \ diff --git a/mpas_analysis/ocean/histogram_ssh.py b/mpas_analysis/ocean/histogram_ssh.py new file mode 100644 index 000000000..739a4bbc1 --- /dev/null +++ b/mpas_analysis/ocean/histogram_ssh.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- +# 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/master/LICENSE +# +import os +import xarray +import numpy +import matplotlib.pyplot as plt + +from mpas_analysis.shared import AnalysisTask + +from mpas_analysis.shared.io import open_mpas_dataset +from mpas_analysis.shared.io.utility import build_config_full_path +from mpas_analysis.shared.climatology import compute_climatology, \ + get_unmasked_mpas_climatology_file_name + +from mpas_analysis.shared.constants import constants +from mpas_analysis.shared.plot import histogram_analysis_plot, savefig +from mpas_analysis.shared.html import write_image_xml + +class HistogramSSH(AnalysisTask): + """ + Plots a histogram of the global mean sea surface height. + + Attributes + ---------- + variableDict : dict + A dictionary of variables from the time series stats monthly output + (keys), together with shorter, more convenient names (values) + + histogramFileName : str + The name of the file where the ssh histogram is stored + + controlConfig : mpas_tools.config.MpasConfigParser + Configuration options for a control run (if one is provided) + + filePrefix : str + The basename (without extension) of the PNG and XML files to write out + """ + # Authors + # ------- + # Xylar Asay-Davis + + def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=None): + + """ + Construct the analysis task. + + Parameters + ---------- + config : mpas_tools.config.MpasConfigParser + Configuration options + + mpasHistogram: ``MpasHistogramTask`` + The task that extracts the time series from MPAS monthly output + + mpasClimatologyTask : ``MpasClimatologyTask`` + The task that produced the climatology to be remapped and plotted + + regionMasksTask : ``ComputeRegionMasks`` + A task for computing region masks + + controlConfig : mpas_tools.config.MpasConfigParser + Configuration options for a control run (if any) + """ + # Authors + # ------- + # Xylar Asay-Davis + + # first, call the constructor from the base class (AnalysisTask) + super().__init__( + config=config, + taskName='histogramSSH', + componentName='ocean', + tags=['climatology', 'regions', 'histogram', 'ssh', 'publicObs']) + + self.run_after(mpasClimatologyTask) + self.mpasClimatologyTask = mpasClimatologyTask + + #self.histogramFileName = '' + self.controlConfig = controlConfig + #TODO make the filePrefix reflect the regionGroup and regionName + self.filePrefix = 'histogram_ssh' + + self.variableDict = {} + for var in ['ssh']: + key = 'timeMonthly_avg_{}'.format(var) + self.variableDict[key] = var + + def setup_and_check(self): + """ + Perform steps to set up the analysis and check for errors in the setup. + + Raises + ------ + OSError + If files are not present + """ + # Authors + # ------- + # Xylar Asay-Davis + + # first, call setup_and_check from the base class (AnalysisTask), + # which will perform some common setup, including storing: + # self.inDirectory, self.plotsDirectory, self.namelist, self.streams + # self.calendar + super().setup_and_check() + + config = self.config + + mainRunName = config.get('runs', 'mainRunName') + regionGroups = config.getexpression(self.taskName, 'regionGroups') + + self.seasons = config.getexpression(self.taskName, 'seasons') + #TODO add to config file + #self.variableList = config.getexpression(self.taskName, 'variableList') + self.variableList = ['timeMonthly_avg_ssh'] + + # Add xml file names for each season + self.xmlFileNames = [] + self.filePrefix = f'ssh_histogram_{mainRunName}' + for season in self.seasons: + self.xmlFileNames.append(f'{self.plotsDirectory}/{self.filePrefix}_{season}.xml') + + # Specify variables and seasons to compute climology over + self.mpasClimatologyTask.add_variables(variableList=self.variableList, + seasons=self.seasons) + + def run_task(self): + """ + Performs histogram analysis of the output of sea-surface height + (SSH). + """ + # Authors + # ------- + # Carolyn Begeman, Adrian Turner, Xylar Asay-Davis + + self.logger.info("\nPlotting histogram of SSH...") + + config = self.config + calendar = self.calendar + seasons = self.seasons + + #startYear = self.startYear + #endYear = self.endYear + #TODO determine whether this is needed + #startDate = '{:04d}-01-01_00:00:00'.format(self.startYear) + #endDate = '{:04d}-12-31_23:59:59'.format(self.endYear) + + mainRunName = config.get('runs', 'mainRunName') + + variableList = ['ssh'] + + baseDirectory = build_config_full_path( + config, 'output', 'histogramSubdirectory') + print(f'baseDirectory={baseDirectory}') + print(f'plotsDirectory={self.plotsDirectory}') + + # the variable mpasFieldName will be added to mpasClimatologyTask + # along with the seasons. + try: + restartFileName = self.runStreams.readpath('restart')[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) + + for season in seasons: + #TODO get the filename of the climatology file from the climatology task + #TODO determine whether this is actually histogramSSH + #TODO make sure that the climatology spans the appropriate years + inFileName = get_unmasked_mpas_climatology_file_name( + config, season, self.componentName, op='avg') + # Use xarray to open climatology dataset + ds = xarray.open_dataset(inFileName) + ds = self._multiply_ssh_by_area(ds) + + #TODO add region specification + #ds.isel(nRegions=self.regionIndex)) + + fields = [ds[variableList[0]]] + #TODO add depth masking + + if config.has_option('histogramSSH', 'lineColors'): + lineColors = [config.get('histogramSSH', 'mainColor')] + else: + lineColors = None + lineWidths = [3] + legendText = [mainRunName] + #TODO add later + #if plotControl: + # fields.append(refData.isel(nRegions=self.regionIndex)) + # lineColors.append(config.get('histogram', 'controlColor')) + # lineWidths.append(1.2) + # legendText.append(controlRunName) + #TODO make title more informative + title = mainRunName + if config.has_option('histogramSSH', 'titleFontSize'): + titleFontSize = config.getint('histogramSSH', + 'titleFontSize') + else: + titleFontSize = None + + if config.has_option('histogramSSH', 'defaultFontSize'): + defaultFontSize = config.getint('histogramSSH', + 'defaultFontSize') + else: + defaultFontSize = None + + yLabel = 'normalized Probability Density Function' + xLabel = f"{ds.ssh.attrs['long_name']} ({ds.ssh.attrs['units']})" + + histogram_analysis_plot(config, fields, calendar=calendar, + title=title, xlabel=xLabel, ylabel=yLabel, + lineColors=lineColors, lineWidths=lineWidths, + legendText=legendText, + titleFontSize=titleFontSize, defaultFontSize=defaultFontSize) + + #TODO whether this should be plotsDirectory or baseDirectory + outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{season}.png' + print(f'outFileName={outFileName}') + savefig(outFileName, config) + + #TODO should this be in the outer loop instead? + caption = 'Normalized probability density function for SSH climatologies in the {} Region'.format(title) + write_image_xml( + config=config, + filePrefix=f'{self.filePrefix}_{season}', + componentName='Ocean', + componentSubdirectory='ocean', + galleryGroup='Histograms', + groupLink='histogramSSH', + gallery='SSH Histogram', + thumbnailDescription=title, + imageDescription=caption, + imageCaption=caption) + + + def _multiply_ssh_by_area(self, ds): + + """ + Compute a time series of the global mean water-column thickness. + """ + + restartFileName = self.runStreams.readpath('restart')[0] + + dsRestart = xarray.open_dataset(restartFileName) + dsRestart = dsRestart.isel(Time=0) + + #TODO load seaIceArea for sea ice histograms + #landIceFraction = dsRestart.landIceFraction.isel(Time=0) + areaCell = dsRestart.areaCell + + # for convenience, rename the variables to simpler, shorter names + ds = ds.rename(self.variableDict) + + ds['sshAreaCell'] = \ + ds['ssh'] / areaCell + ds.sshAreaCell.attrs['units'] = 'm^2' + ds.sshAreaCell.attrs['description'] = \ + 'Sea-surface height multiplied by the cell area' + + return ds diff --git a/mpas_analysis/shared/plot/__init__.py b/mpas_analysis/shared/plot/__init__.py index 2545f375f..85bec63bf 100644 --- a/mpas_analysis/shared/plot/__init__.py +++ b/mpas_analysis/shared/plot/__init__.py @@ -7,6 +7,8 @@ from mpas_analysis.shared.plot.vertical_section import \ plot_vertical_section_comparison, plot_vertical_section +from mpas_analysis.shared.plot.histogram import histogram_analysis_plot + from mpas_analysis.shared.plot.oned import plot_1D from mpas_analysis.shared.plot.save import savefig diff --git a/mpas_analysis/shared/plot/histogram.py b/mpas_analysis/shared/plot/histogram.py new file mode 100644 index 000000000..b1a87a683 --- /dev/null +++ b/mpas_analysis/shared/plot/histogram.py @@ -0,0 +1,176 @@ +# 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/master/LICENSE +""" +Functions for plotting histograms (and comparing with reference data sets) +""" +# Authors +# ------- +# Carolyn Begeman, Adrian Turner, Xylar Asay-Davis + +import matplotlib +import matplotlib.pyplot as plt +import xarray as xr +import pandas as pd +import numpy as np + +from mpas_analysis.shared.timekeeping.utility import date_to_days + +from mpas_analysis.shared.constants import constants + +from mpas_analysis.shared.plot.ticks import plot_xtick_format +from mpas_analysis.shared.plot.title import limit_title + +def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, + density=True, lineColors=None, + lineStyles=None, markers=None, lineWidths=None, + legendText=None, + titleFontSize=None, axisFontSize=None, defaultFontSize=None, + figsize=(12, 6), dpi=None, + legendLocation='upper right', + maxTitleLength=90): + + """ + Plots the list of histogram data sets. + + Parameters + ---------- + config : instance of ConfigParser + the configuration, containing a [plot] section with options that + control plotting + + dsvalues : list of xarray DataSets + the data set(s) to be plotted. For area or volume quantities, multiply by the + respective area or volume before inputting. Datasets should already be sliced + within the time range specified in the config file. + + title : str + the title of the plot + + xlabel, ylabel : str + axis labels + + calendar : str + the calendar to use for formatting the time axis + + density : logical + if True, normalize the histogram so that the area under the curve is 1 + + lineColors, lineStyles, legendText : list of str, optional + control line color, style, and corresponding legend + text. Default is black, solid line, and no legend. + + lineWidths : list of float, optional + control line width. Default is 1.0. + + titleFontSize : int, optional + the size of the title font + + defaultFontSize : int, optional + the size of text other than the title + + figsize : tuple of float, optional + the size of the figure in inches + + dpi : int, optional + the number of dots per inch of the figure, taken from section ``plot`` + option ``dpi`` in the config file by default + + legendLocation : str, optional + The location of the legend (see ``pyplot.legend()`` for details) + + maxTitleLength : int, optional + the maximum number of characters in the title and legend, beyond which + they are truncated with a trailing ellipsis + + Returns + ------- + fig : ``matplotlib.figure.Figure`` + The resulting figure + """ + # Authors + # ------- + # Carolyn Begeman, Adrian Turner, Xylar Asay-Davis + + if defaultFontSize is None: + defaultFontSize = config.getint('plot', 'defaultFontSize') + matplotlib.rc('font', size=defaultFontSize) + + if dpi is None: + dpi = config.getint('plot', 'dpi') + + fig = plt.figure(figsize=figsize, dpi=dpi) + #TODO ensure that this is appropriate for this plot type + if title is not None: + if titleFontSize is None: + titleFontSize = config.get('plot', 'threePanelTitleFontSize') + title_font = {'size': titleFontSize, + 'color': config.get('plot', 'threePanelTitleFontColor'), + 'weight': config.get('plot', + 'threePanelTitleFontWeight')} + # suptitle = fig.suptitle(title, y=0.99, **title_font) + #else: + # suptitle = None + if axisFontSize is None: + axisFontSize = config.get('plot', 'axisFontSize') + axis_font = {'size': axisFontSize} + + + ax = plt.gca() + labelCount = 0 + for dsIndex in range(len(dsvalues)): + dsvalue = dsvalues[dsIndex] + if dsvalue is None: + continue + if legendText is None: + label = None + else: + label = legendText[dsIndex] + if label is not None: + label = limit_title(label, maxTitleLength) + labelCount += 1 + if lineColors is None: + color = 'k' + else: + color = lineColors[dsIndex] + if lineStyles is None: + linestyle = '-' + else: + linestyle = lineStyles[dsIndex] + if markers is None: + marker = None + else: + marker = markers[dsIndex] + if lineWidths is None: + linewidth = 1. + else: + linewidth = lineWidths[dsIndex] + + # TODO read varRange from a config file + hist_val_range = None + # TODO read hist_bins from a config file + hist_bins = 20 + hist_values = dsvalue.values.ravel() + hist_type = 'step' + ax.hist(hist_values, range=hist_val_range, bins=hist_bins, + linestyle=linestyle, linewidth=linewidth, + histtype=hist_type, label=label, density=density) + if labelCount > 1: + plt.legend(loc=legendLocation) + + if title is not None: + title = limit_title(title, maxTitleLength) + plt.title(title, **title_font) + if xlabel is not None: + plt.xlabel(xlabel, **axis_font) + if ylabel is not None: + plt.ylabel(ylabel, **axis_font) + + return fig From 0f3f5cb01b6cf8e6822819996e5860a78c9ed397 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Fri, 23 Sep 2022 17:43:26 -0500 Subject: [PATCH 02/26] Generalize histogram task to 2-d ocean variables --- mpas_analysis/__main__.py | 2 +- mpas_analysis/ocean/__init__.py | 2 +- .../ocean/{histogram_ssh.py => histogram.py} | 162 +++++++++--------- 3 files changed, 85 insertions(+), 81 deletions(-) rename mpas_analysis/ocean/{histogram_ssh.py => histogram.py} (62%) diff --git a/mpas_analysis/__main__.py b/mpas_analysis/__main__.py index 154a298b3..e958cdbbf 100755 --- a/mpas_analysis/__main__.py +++ b/mpas_analysis/__main__.py @@ -197,7 +197,7 @@ def build_analysis_list(config, controlConfig): controlConfig)) analyses.append(ocean.TimeSeriesTransport(config, controlConfig)) - analyses.append(ocean.HistogramSSH(config, oceanClimatolgyTasks['avg'], oceanRegionMasksTask, + analyses.append(ocean.OceanHistogram(config, oceanClimatolgyTasks['avg'], oceanRegionMasksTask, controlConfig)) analyses.append(ocean.MeridionalHeatTransport( config, oceanClimatologyTasks['avg'], controlConfig)) diff --git a/mpas_analysis/ocean/__init__.py b/mpas_analysis/ocean/__init__.py index df5a0e50a..18b5468d0 100644 --- a/mpas_analysis/ocean/__init__.py +++ b/mpas_analysis/ocean/__init__.py @@ -31,7 +31,7 @@ from mpas_analysis.ocean.time_series_ssh_anomaly import TimeSeriesSSHAnomaly from mpas_analysis.ocean.time_series_sst import TimeSeriesSST -from mpas_analysis.ocean.histogram_ssh import HistogramSSH +from mpas_analysis.ocean.histogram import OceanHistogram from mpas_analysis.ocean.index_nino34 import IndexNino34 from mpas_analysis.ocean.streamfunction_moc import StreamfunctionMOC from mpas_analysis.ocean.meridional_heat_transport import \ diff --git a/mpas_analysis/ocean/histogram_ssh.py b/mpas_analysis/ocean/histogram.py similarity index 62% rename from mpas_analysis/ocean/histogram_ssh.py rename to mpas_analysis/ocean/histogram.py index 739a4bbc1..6ec126a1c 100644 --- a/mpas_analysis/ocean/histogram_ssh.py +++ b/mpas_analysis/ocean/histogram.py @@ -26,9 +26,9 @@ from mpas_analysis.shared.plot import histogram_analysis_plot, savefig from mpas_analysis.shared.html import write_image_xml -class HistogramSSH(AnalysisTask): +class OceanHistogram(AnalysisTask): """ - Plots a histogram of the global mean sea surface height. + Plots a histogram of a 2-d ocean variable. Attributes ---------- @@ -37,7 +37,7 @@ class HistogramSSH(AnalysisTask): (keys), together with shorter, more convenient names (values) histogramFileName : str - The name of the file where the ssh histogram is stored + The name of the file where the histogram is stored controlConfig : mpas_tools.config.MpasConfigParser Configuration options for a control run (if one is provided) @@ -78,22 +78,15 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N # first, call the constructor from the base class (AnalysisTask) super().__init__( config=config, - taskName='histogramSSH', + taskName='oceanHistogram', componentName='ocean', - tags=['climatology', 'regions', 'histogram', 'ssh', 'publicObs']) + tags=['climatology', 'regions', 'histogram', 'publicObs']) self.run_after(mpasClimatologyTask) self.mpasClimatologyTask = mpasClimatologyTask #self.histogramFileName = '' self.controlConfig = controlConfig - #TODO make the filePrefix reflect the regionGroup and regionName - self.filePrefix = 'histogram_ssh' - - self.variableDict = {} - for var in ['ssh']: - key = 'timeMonthly_avg_{}'.format(var) - self.variableDict[key] = var def setup_and_check(self): """ @@ -117,33 +110,40 @@ def setup_and_check(self): config = self.config mainRunName = config.get('runs', 'mainRunName') + + #TODO make the filePrefix reflect the regionGroup and regionName + self.filePrefix = f'histogram_{mainRunName}' + regionGroups = config.getexpression(self.taskName, 'regionGroups') self.seasons = config.getexpression(self.taskName, 'seasons') - #TODO add to config file - #self.variableList = config.getexpression(self.taskName, 'variableList') - self.variableList = ['timeMonthly_avg_ssh'] + self.variableList = config.getexpression(self.taskName, 'variableList') - # Add xml file names for each season - self.xmlFileNames = [] - self.filePrefix = f'ssh_histogram_{mainRunName}' - for season in self.seasons: - self.xmlFileNames.append(f'{self.plotsDirectory}/{self.filePrefix}_{season}.xml') + variableList = [] + self.variableDict = {} + for var in self.variableList: + key = f'timeMonthly_avg_{var}' + variableList.append(key) + self.variableDict[key] = var + + # Add xml file names for each season + self.xmlFileNames = [] + for season in self.seasons: + self.xmlFileNames.append(f'{self.plotsDirectory}/{self.filePrefix}_{var}_{season}.xml') # Specify variables and seasons to compute climology over - self.mpasClimatologyTask.add_variables(variableList=self.variableList, + self.mpasClimatologyTask.add_variables(variableList=variableList, seasons=self.seasons) def run_task(self): """ - Performs histogram analysis of the output of sea-surface height - (SSH). + Performs histogram analysis of the output of variables in variableList. """ # Authors # ------- # Carolyn Begeman, Adrian Turner, Xylar Asay-Davis - self.logger.info("\nPlotting histogram of SSH...") + self.logger.info("\nPlotting histogram of ocean vars...") config = self.config calendar = self.calendar @@ -157,8 +157,6 @@ def run_task(self): mainRunName = config.get('runs', 'mainRunName') - variableList = ['ssh'] - baseDirectory = build_config_full_path( config, 'output', 'histogramSubdirectory') print(f'baseDirectory={baseDirectory}') @@ -176,76 +174,78 @@ def run_task(self): for season in seasons: #TODO get the filename of the climatology file from the climatology task - #TODO determine whether this is actually histogramSSH #TODO make sure that the climatology spans the appropriate years inFileName = get_unmasked_mpas_climatology_file_name( config, season, self.componentName, op='avg') # Use xarray to open climatology dataset ds = xarray.open_dataset(inFileName) - ds = self._multiply_ssh_by_area(ds) + ds = self._multiply_var_by_area(ds, self.variableList) #TODO add region specification #ds.isel(nRegions=self.regionIndex)) - - fields = [ds[variableList[0]]] - #TODO add depth masking - - if config.has_option('histogramSSH', 'lineColors'): - lineColors = [config.get('histogramSSH', 'mainColor')] + if config.has_option(self.taskName, 'lineColors'): + lineColors = [config.get(self.taskName, 'mainColor')] else: lineColors = None lineWidths = [3] legendText = [mainRunName] - #TODO add later - #if plotControl: - # fields.append(refData.isel(nRegions=self.regionIndex)) - # lineColors.append(config.get('histogram', 'controlColor')) - # lineWidths.append(1.2) - # legendText.append(controlRunName) - #TODO make title more informative + title = mainRunName - if config.has_option('histogramSSH', 'titleFontSize'): - titleFontSize = config.getint('histogramSSH', + if config.has_option(self.taskName, 'titleFontSize'): + titleFontSize = config.getint(self.taskName, 'titleFontSize') else: titleFontSize = None - if config.has_option('histogramSSH', 'defaultFontSize'): - defaultFontSize = config.getint('histogramSSH', + if config.has_option(self.taskName, 'defaultFontSize'): + defaultFontSize = config.getint(self.taskName, 'defaultFontSize') else: defaultFontSize = None yLabel = 'normalized Probability Density Function' - xLabel = f"{ds.ssh.attrs['long_name']} ({ds.ssh.attrs['units']})" - - histogram_analysis_plot(config, fields, calendar=calendar, - title=title, xlabel=xLabel, ylabel=yLabel, - lineColors=lineColors, lineWidths=lineWidths, - legendText=legendText, - titleFontSize=titleFontSize, defaultFontSize=defaultFontSize) - - #TODO whether this should be plotsDirectory or baseDirectory - outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{season}.png' - print(f'outFileName={outFileName}') - savefig(outFileName, config) - - #TODO should this be in the outer loop instead? - caption = 'Normalized probability density function for SSH climatologies in the {} Region'.format(title) - write_image_xml( - config=config, - filePrefix=f'{self.filePrefix}_{season}', - componentName='Ocean', - componentSubdirectory='ocean', - galleryGroup='Histograms', - groupLink='histogramSSH', - gallery='SSH Histogram', - thumbnailDescription=title, - imageDescription=caption, - imageCaption=caption) - - - def _multiply_ssh_by_area(self, ds): + + for var in self.variableList: + + fields = [ds[var]] + #TODO add depth masking + + #TODO add later + #if plotControl: + # fields.append(refData.isel(nRegions=self.regionIndex)) + # lineColors.append(config.get('histogram', 'controlColor')) + # lineWidths.append(1.2) + # legendText.append(controlRunName) + #TODO make title more informative + xLabel = f"{ds.ssh.attrs['long_name']} ({ds.ssh.attrs['units']})" + + histogram_analysis_plot(config, fields, calendar=calendar, + title=title, xlabel=xLabel, ylabel=yLabel, + lineColors=lineColors, lineWidths=lineWidths, + legendText=legendText, + titleFontSize=titleFontSize, defaultFontSize=defaultFontSize) + + #TODO whether this should be plotsDirectory or baseDirectory + outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{var}_{season}.png' + print(f'outFileName={outFileName}') + savefig(outFileName, config) + + #TODO should this be in the outer loop instead? + caption = 'Normalized probability density function for SSH climatologies in the {} Region'.format(title) + write_image_xml( + config=config, + filePrefix=f'{self.filePrefix}_{var}_{season}', + componentName='Ocean', + componentSubdirectory='ocean', + galleryGroup='Histograms', + groupLink=f'histogram{var}', + gallery=f'{var} Histogram', + thumbnailDescription=title, + imageDescription=caption, + imageCaption=caption) + + + def _multiply_var_by_area(self, ds, variableList): """ Compute a time series of the global mean water-column thickness. @@ -263,10 +263,14 @@ def _multiply_ssh_by_area(self, ds): # for convenience, rename the variables to simpler, shorter names ds = ds.rename(self.variableDict) - ds['sshAreaCell'] = \ - ds['ssh'] / areaCell - ds.sshAreaCell.attrs['units'] = 'm^2' - ds.sshAreaCell.attrs['description'] = \ - 'Sea-surface height multiplied by the cell area' + for varName in variableList: + #varName = {i for i in self.variableDict if self.variableDict[i]==var} + varAreaName = f'{varName}AreaCell' + ds[varAreaName] = ds[varName] / areaCell + ds[varAreaName].attrs['units'] = 'm^2' + #ds.sshAreaCell.attrs['units'] = 'm^2' + #ds.sshAreaCell.attrs['description'] = \ + ds[varAreaName].attrs['description'] = \ + f'{varName} multiplied by the cell area' return ds From ba71bda4ef1d19a105cfdb46fa02a6a959817658 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Mon, 26 Sep 2022 11:37:52 -0500 Subject: [PATCH 03/26] Enhance ocean histogram capabilities --- mpas_analysis/ocean/histogram.py | 67 ++++++++++++++++---------- mpas_analysis/shared/plot/histogram.py | 23 +++------ 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 6ec126a1c..fc2bd8d48 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -18,7 +18,7 @@ from mpas_analysis.shared import AnalysisTask from mpas_analysis.shared.io import open_mpas_dataset -from mpas_analysis.shared.io.utility import build_config_full_path +from mpas_analysis.shared.io.utility import build_config_full_path, build_obs_path from mpas_analysis.shared.climatology import compute_climatology, \ get_unmasked_mpas_climatology_file_name @@ -85,9 +85,9 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N self.run_after(mpasClimatologyTask) self.mpasClimatologyTask = mpasClimatologyTask - #self.histogramFileName = '' self.controlConfig = controlConfig + def setup_and_check(self): """ Perform steps to set up the analysis and check for errors in the setup. @@ -134,6 +134,20 @@ def setup_and_check(self): # Specify variables and seasons to compute climology over self.mpasClimatologyTask.add_variables(variableList=variableList, seasons=self.seasons) + #TODO fixup + if controlConfig is None: + if 'ssh' in self.variableList: + refTitleLabel = 'Observations (AVISO Dynamic ' \ + 'Topography, 1993-2010)' + + observationsDirectory = build_obs_path( + config, 'ocean', 'sshSubdirectory') + + obsFileName = \ + "{}/zos_AVISO_L4_199210-201012_20180710.nc".format( + observationsDirectory) + print(obsFileName) + def run_task(self): """ @@ -149,12 +163,6 @@ def run_task(self): calendar = self.calendar seasons = self.seasons - #startYear = self.startYear - #endYear = self.endYear - #TODO determine whether this is needed - #startDate = '{:04d}-01-01_00:00:00'.format(self.startYear) - #endDate = '{:04d}-12-31_23:59:59'.format(self.endYear) - mainRunName = config.get('runs', 'mainRunName') baseDirectory = build_config_full_path( @@ -169,17 +177,18 @@ def run_task(self): 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 config.has_option(self.taskName, 'areaVarName'): + areaVarName = config.get(self.taskName, 'areaVarName') + else: + areaVarName = 'areaCell' for season in seasons: - #TODO get the filename of the climatology file from the climatology task - #TODO make sure that the climatology spans the appropriate years inFileName = get_unmasked_mpas_climatology_file_name( config, season, self.componentName, op='avg') # Use xarray to open climatology dataset ds = xarray.open_dataset(inFileName) - ds = self._multiply_var_by_area(ds, self.variableList) + ds = self._multiply_var_by_area(ds, self.variableList, areaVarName=areaVarName) #TODO add region specification #ds.isel(nRegions=self.regionIndex)) @@ -196,19 +205,28 @@ def run_task(self): 'titleFontSize') else: titleFontSize = None + if config.has_option(self.taskName, 'titleFontSize'): + axisFontSize = config.getint(self.taskName, + 'axisFontSize') + else: + axisFontSize = None if config.has_option(self.taskName, 'defaultFontSize'): defaultFontSize = config.getint(self.taskName, 'defaultFontSize') else: defaultFontSize = None + if config.has_option(self.taskName, 'bins'): + bins = config.getint(self.taskName, 'bins') + else: + bins = None yLabel = 'normalized Probability Density Function' for var in self.variableList: - fields = [ds[var]] - #TODO add depth masking + fields = [ds[f'{var}_{areaVarName}']] + #Note: if we want to support 3-d variable histograms, we need to add depth masking #TODO add later #if plotControl: @@ -220,14 +238,12 @@ def run_task(self): xLabel = f"{ds.ssh.attrs['long_name']} ({ds.ssh.attrs['units']})" histogram_analysis_plot(config, fields, calendar=calendar, - title=title, xlabel=xLabel, ylabel=yLabel, + title=title, xlabel=xLabel, ylabel=yLabel, bins=bins, lineColors=lineColors, lineWidths=lineWidths, legendText=legendText, titleFontSize=titleFontSize, defaultFontSize=defaultFontSize) - #TODO whether this should be plotsDirectory or baseDirectory outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{var}_{season}.png' - print(f'outFileName={outFileName}') savefig(outFileName, config) #TODO should this be in the outer loop instead? @@ -245,7 +261,7 @@ def run_task(self): imageCaption=caption) - def _multiply_var_by_area(self, ds, variableList): + def _multiply_var_by_area(self, ds, variableList, areaVarName='areaCell', fractionVarName=None): """ Compute a time series of the global mean water-column thickness. @@ -256,20 +272,19 @@ def _multiply_var_by_area(self, ds, variableList): dsRestart = xarray.open_dataset(restartFileName) dsRestart = dsRestart.isel(Time=0) - #TODO load seaIceArea for sea ice histograms - #landIceFraction = dsRestart.landIceFraction.isel(Time=0) - areaCell = dsRestart.areaCell + areaCell = dsRestart[areaVarName] + print(f'shape(areaCell) = {numpy.shape(areaCell.values)}') # for convenience, rename the variables to simpler, shorter names ds = ds.rename(self.variableDict) for varName in variableList: - #varName = {i for i in self.variableDict if self.variableDict[i]==var} - varAreaName = f'{varName}AreaCell' + varAreaName = f'{varName}_{areaVarName}' ds[varAreaName] = ds[varName] / areaCell + if fractionVarName is not None: + frac = ds[fractionVarName] + ds[varAreaName] = ds[varName] * frac ds[varAreaName].attrs['units'] = 'm^2' - #ds.sshAreaCell.attrs['units'] = 'm^2' - #ds.sshAreaCell.attrs['description'] = \ ds[varAreaName].attrs['description'] = \ f'{varName} multiplied by the cell area' diff --git a/mpas_analysis/shared/plot/histogram.py b/mpas_analysis/shared/plot/histogram.py index b1a87a683..c6bc9a859 100644 --- a/mpas_analysis/shared/plot/histogram.py +++ b/mpas_analysis/shared/plot/histogram.py @@ -29,7 +29,7 @@ from mpas_analysis.shared.plot.title import limit_title def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, - density=True, lineColors=None, + bins=20, range=None, density=True, lineColors=None, lineStyles=None, markers=None, lineWidths=None, legendText=None, titleFontSize=None, axisFontSize=None, defaultFontSize=None, @@ -107,26 +107,19 @@ def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, dpi = config.getint('plot', 'dpi') fig = plt.figure(figsize=figsize, dpi=dpi) - #TODO ensure that this is appropriate for this plot type if title is not None: if titleFontSize is None: - titleFontSize = config.get('plot', 'threePanelTitleFontSize') + titleFontSize = config.get('plot', 'titleFontSize') title_font = {'size': titleFontSize, - 'color': config.get('plot', 'threePanelTitleFontColor'), - 'weight': config.get('plot', - 'threePanelTitleFontWeight')} - # suptitle = fig.suptitle(title, y=0.99, **title_font) - #else: - # suptitle = None + 'color': config.get('plot', 'titleFontColor'), + 'weight': config.get('plot', 'titleFontWeight')} if axisFontSize is None: axisFontSize = config.get('plot', 'axisFontSize') axis_font = {'size': axisFontSize} - ax = plt.gca() labelCount = 0 - for dsIndex in range(len(dsvalues)): - dsvalue = dsvalues[dsIndex] + for dsIndex, dsvalue in enumerate(dsvalues): if dsvalue is None: continue if legendText is None: @@ -153,13 +146,9 @@ def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, else: linewidth = lineWidths[dsIndex] - # TODO read varRange from a config file - hist_val_range = None - # TODO read hist_bins from a config file - hist_bins = 20 hist_values = dsvalue.values.ravel() hist_type = 'step' - ax.hist(hist_values, range=hist_val_range, bins=hist_bins, + ax.hist(hist_values, range=range, bins=bins, linestyle=linestyle, linewidth=linewidth, histtype=hist_type, label=label, density=density) if labelCount > 1: From ec47ac63a8836ee9519d10df623c6707d39f479a Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Mon, 26 Sep 2022 14:00:55 -0500 Subject: [PATCH 04/26] Support regions list for ocean histogram --- mpas_analysis/ocean/histogram.py | 562 ++++++++++++++++++++++++------- 1 file changed, 432 insertions(+), 130 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index fc2bd8d48..d404bb900 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -18,7 +18,7 @@ from mpas_analysis.shared import AnalysisTask from mpas_analysis.shared.io import open_mpas_dataset -from mpas_analysis.shared.io.utility import build_config_full_path, build_obs_path +from mpas_analysis.shared.io.utility import build_config_full_path, build_obs_path, decode_strings from mpas_analysis.shared.climatology import compute_climatology, \ get_unmasked_mpas_climatology_file_name @@ -86,8 +86,69 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N self.mpasClimatologyTask = mpasClimatologyTask self.controlConfig = controlConfig + mainRunName = config.get('runs', 'mainRunName') + + self.regionGroups = config.getexpression(self.taskName, 'regionGroups') + self.regionNames = config.getexpression(self.taskName, 'regionNames') + self.seasons = config.getexpression(self.taskName, 'seasons') + self.variableList = config.getexpression(self.taskName, 'variableList') + self.filePrefix = f'histogram_{mainRunName}' + obsList = [] + #TODO add gridName + obsDicts = { + 'AVISO': { + 'suffix': 'AVISO', + 'gridName': 'Global_1.0x1.0degree', + 'gridFileName': 'SSH/zos_AVISO_L4_199210-201012_20180710.nc', + 'lonVar': 'lon', + 'latVar': 'lat', + 'sshVar': 'zos'}} + + #variableList = [] + #self.variableDict = {} + #for var in self.variableList: + # key = f'timeMonthly_avg_{var}' + # variableList.append(key) + # self.variableDict[key] = var + + for regionGroup in self.regionGroups: + groupObsDicts = {} + print(f'Add mask subtask for {regionGroup}, MPAS') + mpasMasksSubtask = regionMasksTask.add_mask_subtask( + regionGroup=regionGroup) + regionNames = mpasMasksSubtask.expand_region_names(self.regionNames) + if controlConfig is None: + for obsName in obsList: + localObsDict = dict(obsDicts[obsName]) + obsFileName = build_obs_path( + config, component=self.componentName, + relativePath=localObsDict['gridFileName']) + print(f'Add mask subtask for {regionGroup}, {obsName}') + obsMasksSubtask = regionMasksTask.add_mask_subtask( + regionGroup, obsFileName=obsFileName, + lonVar=localObsDict['lonVar'], + latVar=localObsDict['latVar'], + meshName=localObsDict['gridName']) + obsDicts[obsName]['maskTask'] = obsMasksSubtask + + localObsDict['maskTask'] = obsMasksSubtask + groupObsDicts[obsName] = localObsDict + for var in self.variableList: + localVarDict = dict({var: f'timeMonthly_avg_{var}'}) + for regionName in regionNames: + sectionName = None + fullSuffix = self.filePrefix + for season in self.seasons: + plotRegionSubtask = PlotRegionHistogramSubtask( + self, regionGroup, regionName, controlConfig, + sectionName, fullSuffix, mpasClimatologyTask, + mpasMasksSubtask, groupObsDicts, localVarDict, season) + plotRegionSubtask.run_after(mpasMasksSubtask) + self.add_subtask(plotRegionSubtask) + print(f'Add regional histogram subtask for {var}, {regionName}, {season}') + def setup_and_check(self): """ Perform steps to set up the analysis and check for errors in the setup. @@ -109,16 +170,8 @@ def setup_and_check(self): config = self.config - mainRunName = config.get('runs', 'mainRunName') - - #TODO make the filePrefix reflect the regionGroup and regionName - self.filePrefix = f'histogram_{mainRunName}' - regionGroups = config.getexpression(self.taskName, 'regionGroups') - self.seasons = config.getexpression(self.taskName, 'seasons') - self.variableList = config.getexpression(self.taskName, 'variableList') - variableList = [] self.variableDict = {} for var in self.variableList: @@ -126,27 +179,18 @@ def setup_and_check(self): variableList.append(key) self.variableDict[key] = var - # Add xml file names for each season - self.xmlFileNames = [] - for season in self.seasons: - self.xmlFileNames.append(f'{self.plotsDirectory}/{self.filePrefix}_{var}_{season}.xml') + #for var in self.variableList: + # # Add xml file names for each season + # self.xmlFileNames = [] + # for season in self.seasons: + # for regionName in self.regionNames: + # print(f'add xml from main: {self.plotsDirectory}/{self.filePrefix}_{var}_{regionName}_{season}.xml') + # self.xmlFileNames.append(f'{self.plotsDirectory}/{self.filePrefix}_{var}_{regionName}_{season}.xml') # Specify variables and seasons to compute climology over + print(f'add climatology variables') self.mpasClimatologyTask.add_variables(variableList=variableList, seasons=self.seasons) - #TODO fixup - if controlConfig is None: - if 'ssh' in self.variableList: - refTitleLabel = 'Observations (AVISO Dynamic ' \ - 'Topography, 1993-2010)' - - observationsDirectory = build_obs_path( - config, 'ocean', 'sshSubdirectory') - - obsFileName = \ - "{}/zos_AVISO_L4_199210-201012_20180710.nc".format( - observationsDirectory) - print(obsFileName) def run_task(self): @@ -157,116 +201,116 @@ def run_task(self): # ------- # Carolyn Begeman, Adrian Turner, Xylar Asay-Davis - self.logger.info("\nPlotting histogram of ocean vars...") - - config = self.config - calendar = self.calendar - seasons = self.seasons - - mainRunName = config.get('runs', 'mainRunName') - - baseDirectory = build_config_full_path( - config, 'output', 'histogramSubdirectory') - print(f'baseDirectory={baseDirectory}') - print(f'plotsDirectory={self.plotsDirectory}') - - # the variable mpasFieldName will be added to mpasClimatologyTask - # along with the seasons. - try: - restartFileName = self.runStreams.readpath('restart')[0] - except ValueError: - raise IOError('No MPAS-O restart file found: need at least one' - ' restart file to plot T-S diagrams') - - if config.has_option(self.taskName, 'areaVarName'): - areaVarName = config.get(self.taskName, 'areaVarName') - else: - areaVarName = 'areaCell' - - for season in seasons: - inFileName = get_unmasked_mpas_climatology_file_name( - config, season, self.componentName, op='avg') - # Use xarray to open climatology dataset - ds = xarray.open_dataset(inFileName) - ds = self._multiply_var_by_area(ds, self.variableList, areaVarName=areaVarName) - - #TODO add region specification - #ds.isel(nRegions=self.regionIndex)) - if config.has_option(self.taskName, 'lineColors'): - lineColors = [config.get(self.taskName, 'mainColor')] - else: - lineColors = None - lineWidths = [3] - legendText = [mainRunName] - - title = mainRunName - if config.has_option(self.taskName, 'titleFontSize'): - titleFontSize = config.getint(self.taskName, - 'titleFontSize') - else: - titleFontSize = None - if config.has_option(self.taskName, 'titleFontSize'): - axisFontSize = config.getint(self.taskName, - 'axisFontSize') - else: - axisFontSize = None - - if config.has_option(self.taskName, 'defaultFontSize'): - defaultFontSize = config.getint(self.taskName, - 'defaultFontSize') - else: - defaultFontSize = None - if config.has_option(self.taskName, 'bins'): - bins = config.getint(self.taskName, 'bins') - else: - bins = None - - yLabel = 'normalized Probability Density Function' - - for var in self.variableList: - - fields = [ds[f'{var}_{areaVarName}']] - #Note: if we want to support 3-d variable histograms, we need to add depth masking - - #TODO add later - #if plotControl: - # fields.append(refData.isel(nRegions=self.regionIndex)) - # lineColors.append(config.get('histogram', 'controlColor')) - # lineWidths.append(1.2) - # legendText.append(controlRunName) - #TODO make title more informative - xLabel = f"{ds.ssh.attrs['long_name']} ({ds.ssh.attrs['units']})" - - histogram_analysis_plot(config, fields, calendar=calendar, - title=title, xlabel=xLabel, ylabel=yLabel, bins=bins, - lineColors=lineColors, lineWidths=lineWidths, - legendText=legendText, - titleFontSize=titleFontSize, defaultFontSize=defaultFontSize) - - outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{var}_{season}.png' - savefig(outFileName, config) - - #TODO should this be in the outer loop instead? - caption = 'Normalized probability density function for SSH climatologies in the {} Region'.format(title) - write_image_xml( - config=config, - filePrefix=f'{self.filePrefix}_{var}_{season}', - componentName='Ocean', - componentSubdirectory='ocean', - galleryGroup='Histograms', - groupLink=f'histogram{var}', - gallery=f'{var} Histogram', - thumbnailDescription=title, - imageDescription=caption, - imageCaption=caption) - + #FIXUP do nothing here? + print(f'run main task') + #self.logger.info("\nPlotting histogram of ocean vars...") + + #config = self.config + #calendar = self.calendar + #seasons = self.seasons + + #mainRunName = config.get('runs', 'mainRunName') + + #baseDirectory = build_config_full_path( + # config, 'output', 'histogramSubdirectory') + #print(f'baseDirectory={baseDirectory}') + #print(f'plotsDirectory={self.plotsDirectory}') + + ## the variable mpasFieldName will be added to mpasClimatologyTask + ## along with the seasons. + #try: + # restartFileName = self.runStreams.readpath('restart')[0] + #except ValueError: + # raise IOError('No MPAS-O restart file found: need at least one' + # ' restart file to plot T-S diagrams') + + #if config.has_option(self.taskName, 'areaVarName'): + # areaVarName = config.get(self.taskName, 'areaVarName') + #else: + # areaVarName = 'areaCell' + + #for season in seasons: + # inFileName = get_unmasked_mpas_climatology_file_name( + # config, season, self.componentName, op='avg') + # # Use xarray to open climatology dataset + # ds = xarray.open_dataset(inFileName) + # ds = self._multiply_var_by_area(ds, self.variableList, areaVarName=areaVarName) + + # #TODO add region specification + # #ds.isel(nRegions=self.regionIndex)) + # if config.has_option(self.taskName, 'lineColors'): + # lineColors = [config.get(self.taskName, 'mainColor')] + # else: + # lineColors = None + # lineWidths = [3] + # legendText = [mainRunName] + + # title = mainRunName + # if config.has_option(self.taskName, 'titleFontSize'): + # titleFontSize = config.getint(self.taskName, + # 'titleFontSize') + # else: + # titleFontSize = None + # if config.has_option(self.taskName, 'titleFontSize'): + # axisFontSize = config.getint(self.taskName, + # 'axisFontSize') + # else: + # axisFontSize = None + + # if config.has_option(self.taskName, 'defaultFontSize'): + # defaultFontSize = config.getint(self.taskName, + # 'defaultFontSize') + # else: + # defaultFontSize = None + # if config.has_option(self.taskName, 'bins'): + # bins = config.getint(self.taskName, 'bins') + # else: + # bins = None + + # yLabel = 'normalized Probability Density Function' + + # for var in self.variableList: + + # fields = [ds[f'{var}_{areaVarName}']] + # #Note: if we want to support 3-d variable histograms, we need to add depth masking + + # #TODO add later + # #if plotControl: + # # fields.append(refData.isel(nRegions=self.regionIndex)) + # # lineColors.append(config.get('histogram', 'controlColor')) + # # lineWidths.append(1.2) + # # legendText.append(controlRunName) + # #TODO make title more informative + # xLabel = f"{ds.ssh.attrs['long_name']} ({ds.ssh.attrs['units']})" + + #histogram_analysis_plot(config, fields, calendar=calendar, + # title=title, xlabel=xLabel, ylabel=yLabel, bins=bins, + # lineColors=lineColors, lineWidths=lineWidths, + # legendText=legendText, + # titleFontSize=titleFontSize, defaultFontSize=defaultFontSize) + + #outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{var}_{season}.png' + #savefig(outFileName, config) + + ##TODO should this be in the outer loop instead? + #caption = 'Normalized probability density function for SSH climatologies in the {} Region'.format(title) + #write_image_xml( + # config=config, + # filePrefix=f'{self.filePrefix}_{var}_{season}', + # componentName='Ocean', + # componentSubdirectory='ocean', + # galleryGroup='Histograms', + # groupLink=f'histogram{var}', + # gallery=f'{var} Histogram', + # thumbnailDescription=title, + # imageDescription=caption, + # imageCaption=caption) def _multiply_var_by_area(self, ds, variableList, areaVarName='areaCell', fractionVarName=None): """ Compute a time series of the global mean water-column thickness. """ - restartFileName = self.runStreams.readpath('restart')[0] dsRestart = xarray.open_dataset(restartFileName) @@ -279,6 +323,7 @@ def _multiply_var_by_area(self, ds, variableList, areaVarName='areaCell', fracti ds = ds.rename(self.variableDict) for varName in variableList: + print(f'multiply {varName} by area') varAreaName = f'{varName}_{areaVarName}' ds[varAreaName] = ds[varName] / areaCell if fractionVarName is not None: @@ -289,3 +334,260 @@ def _multiply_var_by_area(self, ds, variableList, areaVarName='areaCell', fracti f'{varName} multiplied by the cell area' return ds + +class PlotRegionHistogramSubtask(AnalysisTask): + """ + Plots a T-S diagram for a given ocean region + + Attributes + ---------- + regionGroup : str + Name of the collection of region to plot + + regionName : str + Name of the region to plot + + sectionName : str + The section of the config file to get options from + + controlConfig : mpas_tools.config.MpasConfigParser + The configuration options for the control run (if any) + + mpasClimatologyTask : ``MpasClimatologyTask`` + The task that produced the climatology to be remapped and plotted + + mpasMasksSubtask : ``ComputeRegionMasksSubtask`` + A task for creating mask MPAS files for each region to plot, used + to get the mask file name + + obsDicts : dict of dicts + Information on the observations to compare against + + varDicts : dict of dicts + Information on the variables to plot + + season : str + The season to compute the climatology for + """ + # Authors + # ------- + # Xylar Asay-Davis + + def __init__(self, parentTask, regionGroup, regionName, controlConfig, + sectionName, fullSuffix, mpasClimatologyTask, + mpasMasksSubtask, obsDicts, varDicts, season): + + """ + Construct the analysis task. + + Parameters + ---------- + parentTask : ``AnalysisTask`` + The parent task, used to get the ``taskName``, ``config`` and + ``componentName`` + + regionGroup : str + Name of the collection of region to plot + + regionName : str + Name of the region to plot + + controlconfig : mpas_tools.config.MpasConfigParser, optional + Configuration options for a control run (if any) + + sectionName : str + The config section with options for this regionGroup + + fullSuffix : str + The regionGroup and regionName combined and modified to be + appropriate as a task or file suffix + + mpasClimatologyTask : ``MpasClimatologyTask`` + The task that produced the climatology to be remapped and plotted + + mpasMasksSubtask : ``ComputeRegionMasksSubtask`` + A task for creating mask MPAS files for each region to plot, used + to get the mask file name + + obsDicts : dict of dicts + Information on the observations to compare agains + + season : str + The season to comput the climatogy for + """ + # Authors + # ------- + # Xylar Asay-Davis + + # first, call the constructor from the base class (AnalysisTask) + print(f'Initialize histogram task') + super(PlotRegionHistogramSubtask, self).__init__( + config=parentTask.config, + taskName=parentTask.taskName, + componentName=parentTask.componentName, + tags=parentTask.tags, + subtaskName=f'plot{fullSuffix}_{regionName}_{season}') + + self.run_after(mpasClimatologyTask) + self.regionGroup = regionGroup + self.regionName = regionName + self.sectionName = sectionName + self.controlConfig = controlConfig + self.mpasClimatologyTask = mpasClimatologyTask + self.mpasMasksSubtask = mpasMasksSubtask + self.obsDicts = obsDicts + self.varDicts = varDicts + self.season = season + self.filePrefix = fullSuffix + + #parallelTaskCount = self.config.getint('execute', 'parallelTaskCount') + #self.subprocessCount = min(parallelTaskCount, + # self.config.getint(self.taskName, + # 'subprocessCount')) + #self.daskThreads = min( + # multiprocessing.cpu_count(), + # self.config.getint(self.taskName, 'daskThreads')) + + def setup_and_check(self): + """ + Perform steps to set up the analysis and check for errors in the setup. + + Raises + ------ + IOError + If files are not present + """ + # Authors + # ------- + # Xylar Asay-Davis + + # first, call setup_and_check from the base class (AnalysisTask), + # which will perform some common setup, including storing: + # self.inDirectory, self.plotsDirectory, self.namelist, self.streams + # self.calendar + super(PlotRegionHistogramSubtask, self).setup_and_check() + + self.xmlFileNames = [] + for var in self.varDicts: + print(f'add xml from subtask: {self.plotsDirectory}/{self.filePrefix}_{var}_{self.regionName}_{self.season}.xml') + # Add xml file names for each season + self.xmlFileNames.append(f'{self.plotsDirectory}/{self.filePrefix}_{var}_{self.regionName}_{self.season}.xml') + + print(f'end subtask setup and check') + + def run_task(self): + """ + Plots time-series output of properties in an ocean region. + """ + # Authors + # ------- + # Xylar Asay-Davis + + self.logger.info("\nPlotting histogram for {}" + "...".format(self.regionName)) + + config = self.config + sectionName = self.sectionName + + regionMaskFile = self.mpasMasksSubtask.geojsonFileName + + self.logger.info(' Make plots...') + + calendar = self.calendar + + mainRunName = config.get('runs', 'mainRunName') + + baseDirectory = build_config_full_path( + config, 'output', 'histogramSubdirectory') + print(f'baseDirectory={baseDirectory}') + print(f'plotsDirectory={self.plotsDirectory}') + + # the variable mpasFieldName will be added to mpasClimatologyTask + # along with the seasons. + #try: + # restartFileName = self.runStreams.readpath('restart')[0] + #except ValueError: + # raise IOError('No MPAS-O restart file found: need at least one' + # ' restart file to plot T-S diagrams') + + regionMaskFileName = self.mpasMasksSubtask.maskFileName + print(f'Open {regionMaskFileName}') + + dsRegionMask = xarray.open_dataset(regionMaskFileName) + + maskRegionNames = decode_strings(dsRegionMask.regionNames) + regionIndex = maskRegionNames.index(self.regionName) + + dsMask = dsRegionMask.isel(nRegions=regionIndex) + cellMask = dsMask.regionCellMasks == 1 + + inFileName = get_unmasked_mpas_climatology_file_name( + config, self.season, self.componentName, op='avg') + # Use xarray to open climatology dataset + print(f'Open {inFileName}') + ds = xarray.open_dataset(inFileName) + ds = ds.where(cellMask, drop=True) + + #TODO add region specification + #ds.isel(nRegions=self.regionIndex)) + if config.has_option(self.taskName, 'lineColors'): + lineColors = [config.get(self.taskName, 'mainColor')] + else: + lineColors = None + lineWidths = [3] + legendText = [mainRunName] + + title = mainRunName + if config.has_option(self.taskName, 'titleFontSize'): + titleFontSize = config.getint(self.taskName, + 'titleFontSize') + else: + titleFontSize = None + if config.has_option(self.taskName, 'titleFontSize'): + axisFontSize = config.getint(self.taskName, + 'axisFontSize') + else: + axisFontSize = None + + if config.has_option(self.taskName, 'defaultFontSize'): + defaultFontSize = config.getint(self.taskName, + 'defaultFontSize') + else: + defaultFontSize = None + if config.has_option(self.taskName, 'bins'): + bins = config.getint(self.taskName, 'bins') + else: + bins = None + + yLabel = 'normalized Probability Density Function' + + for var in self.varDicts: + + varname = f'{self.varDicts[var]}' + fields = [ds[varname]] + xLabel = f"{ds[varname].attrs['long_name']} ({ds[varname].attrs['units']})" + + histogram_analysis_plot(config, fields, calendar=calendar, + title=title, xlabel=xLabel, ylabel=yLabel, bins=bins, + lineColors=lineColors, lineWidths=lineWidths, + legendText=legendText, + titleFontSize=titleFontSize, defaultFontSize=defaultFontSize) + + #fixup + outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{var}_{self.regionName}_{self.season}.png' + savefig(outFileName, config) + + #TODO should this be in the outer loop instead? + caption = 'Normalized probability density function for SSH climatologies in the {} Region'.format(title) + write_image_xml( + config=config, + filePrefix=f'{self.filePrefix}_{var}_{self.regionName}_{self.season}', + componentName='Ocean', + componentSubdirectory='ocean', + galleryGroup='Histograms', + groupLink=f'histogram{var}', + gallery=f'{self.regionGroup} Histograms', + thumbnailDescription=title, + imageDescription=caption, + imageCaption=caption) + From 3fed82868254390121f7dc824d69b367b8366596 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 27 Sep 2022 14:24:33 -0500 Subject: [PATCH 05/26] Support observation list for ocean histogram --- mpas_analysis/ocean/histogram.py | 64 +++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index d404bb900..a1c712030 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -95,7 +95,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N self.filePrefix = f'histogram_{mainRunName}' - obsList = [] + obsList = config.getexpression(self.taskName, 'obsList') #TODO add gridName obsDicts = { 'AVISO': { @@ -105,7 +105,6 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N 'lonVar': 'lon', 'latVar': 'lat', 'sshVar': 'zos'}} - #variableList = [] #self.variableDict = {} #for var in self.variableList: @@ -125,12 +124,13 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N obsFileName = build_obs_path( config, component=self.componentName, relativePath=localObsDict['gridFileName']) - print(f'Add mask subtask for {regionGroup}, {obsName}') + print(f'Add mask subtask for {regionGroup}, {obsName}, {localObsDict["gridName"]}') obsMasksSubtask = regionMasksTask.add_mask_subtask( regionGroup, obsFileName=obsFileName, lonVar=localObsDict['lonVar'], latVar=localObsDict['latVar'], meshName=localObsDict['gridName']) + regionNames = obsMasksSubtask.expand_region_names(self.regionNames) obsDicts[obsName]['maskTask'] = obsMasksSubtask localObsDict['maskTask'] = obsMasksSubtask @@ -144,8 +144,9 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N plotRegionSubtask = PlotRegionHistogramSubtask( self, regionGroup, regionName, controlConfig, sectionName, fullSuffix, mpasClimatologyTask, - mpasMasksSubtask, groupObsDicts, localVarDict, season) + mpasMasksSubtask, obsMasksSubtask, groupObsDicts, localVarDict, season) plotRegionSubtask.run_after(mpasMasksSubtask) + plotRegionSubtask.run_after(obsMasksSubtask) self.add_subtask(plotRegionSubtask) print(f'Add regional histogram subtask for {var}, {regionName}, {season}') @@ -375,7 +376,7 @@ class PlotRegionHistogramSubtask(AnalysisTask): def __init__(self, parentTask, regionGroup, regionName, controlConfig, sectionName, fullSuffix, mpasClimatologyTask, - mpasMasksSubtask, obsDicts, varDicts, season): + mpasMasksSubtask, obsMasksSubtask, obsDicts, varDicts, season): """ Construct the analysis task. @@ -435,6 +436,7 @@ def __init__(self, parentTask, regionGroup, regionName, controlConfig, self.controlConfig = controlConfig self.mpasClimatologyTask = mpasClimatologyTask self.mpasMasksSubtask = mpasMasksSubtask + self.obsMasksSubtask = obsMasksSubtask self.obsDicts = obsDicts self.varDicts = varDicts self.season = season @@ -489,8 +491,6 @@ def run_task(self): config = self.config sectionName = self.sectionName - regionMaskFile = self.mpasMasksSubtask.geojsonFileName - self.logger.info(' Make plots...') calendar = self.calendar @@ -510,6 +510,7 @@ def run_task(self): # raise IOError('No MPAS-O restart file found: need at least one' # ' restart file to plot T-S diagrams') + #regionMaskFile = self.mpasMasksSubtask.geojsonFileName regionMaskFileName = self.mpasMasksSubtask.maskFileName print(f'Open {regionMaskFileName}') @@ -523,6 +524,15 @@ def run_task(self): inFileName = get_unmasked_mpas_climatology_file_name( config, self.season, self.componentName, op='avg') + #TODO: currently does not support len(obsList) > 1 + if len(self.obsDicts) > 0: + obsRegionMaskFileName = self.obsMasksSubtask.maskFileName + dsObsRegionMask = xarray.open_dataset(obsRegionMaskFileName) + maskRegionNames = decode_strings(dsRegionMask.regionNames) + regionIndex = maskRegionNames.index(self.regionName) + + dsObsMask = dsObsRegionMask.isel(nRegions=regionIndex) + obsCellMask = dsObsMask.regionMasks == 1 # Use xarray to open climatology dataset print(f'Open {inFileName}') ds = xarray.open_dataset(inFileName) @@ -534,7 +544,19 @@ def run_task(self): lineColors = [config.get(self.taskName, 'mainColor')] else: lineColors = None - lineWidths = [3] + if config.has_option(self.taskName, 'obsColor'): + obsColor = config.get_expression(self.taskName, 'obsColor') + if lineColors is None: + lineColors = ['b'] + else: + if lineColors is not None: + obsColor = 'k' + + if config.has_option(self.taskName, 'lineWidths'): + lineWidths = [config.get(self.taskName, 'lineWidths')] + else: + lineWidths = None + #lineWidths = [3] legendText = [mainRunName] title = mainRunName @@ -564,9 +586,25 @@ def run_task(self): for var in self.varDicts: varname = f'{self.varDicts[var]}' + #TODO title as attribute or dict of var + varTitle = varname fields = [ds[varname]] xLabel = f"{ds[varname].attrs['long_name']} ({ds[varname].attrs['units']})" - + for obsName in self.obsDicts: + localObsDict = dict(self.obsDicts[obsName]) + obsFileName = build_obs_path( + config, component=self.componentName, + relativePath=localObsDict['gridFileName']) + varnameObs = localObsDict[f'{var}Var'] + print(f'{var} in obs is {varnameObs}') + dsObs = xarray.open_dataset(obsFileName) + dsObs = dsObs.where(obsCellMask, drop=True) + fields.append(dsObs[varnameObs]) + legendText.append(obsName) + if lineColors is not None: + lineColors.append(obsColor) + if lineWidths is not None: + lineWidths.append([lineWidths[0]]) histogram_analysis_plot(config, fields, calendar=calendar, title=title, xlabel=xLabel, ylabel=yLabel, bins=bins, lineColors=lineColors, lineWidths=lineWidths, @@ -578,16 +616,16 @@ def run_task(self): savefig(outFileName, config) #TODO should this be in the outer loop instead? - caption = 'Normalized probability density function for SSH climatologies in the {} Region'.format(title) + caption = f'Normalized probability density function for SSH climatologies in {self.regionName}' write_image_xml( config=config, filePrefix=f'{self.filePrefix}_{var}_{self.regionName}_{self.season}', componentName='Ocean', componentSubdirectory='ocean', - galleryGroup='Histograms', + galleryGroup=f'{self.regionGroup} Histograms', groupLink=f'histogram{var}', - gallery=f'{self.regionGroup} Histograms', - thumbnailDescription=title, + gallery=varTitle, + thumbnailDescription=self.regionName.replace('_', ' '), imageDescription=caption, imageCaption=caption) From 792c5b93c904e95e39785aff58963f9d19ff86c2 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 27 Sep 2022 17:01:06 -0500 Subject: [PATCH 06/26] Use weights in histogram plot --- mpas_analysis/ocean/histogram.py | 51 +++++++------------------- mpas_analysis/shared/plot/histogram.py | 5 ++- 2 files changed, 16 insertions(+), 40 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index a1c712030..80485942a 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -225,11 +225,6 @@ def run_task(self): # raise IOError('No MPAS-O restart file found: need at least one' # ' restart file to plot T-S diagrams') - #if config.has_option(self.taskName, 'areaVarName'): - # areaVarName = config.get(self.taskName, 'areaVarName') - #else: - # areaVarName = 'areaCell' - #for season in seasons: # inFileName = get_unmasked_mpas_climatology_file_name( # config, season, self.componentName, op='avg') @@ -307,35 +302,6 @@ def run_task(self): # imageDescription=caption, # imageCaption=caption) - def _multiply_var_by_area(self, ds, variableList, areaVarName='areaCell', fractionVarName=None): - - """ - Compute a time series of the global mean water-column thickness. - """ - restartFileName = self.runStreams.readpath('restart')[0] - - dsRestart = xarray.open_dataset(restartFileName) - dsRestart = dsRestart.isel(Time=0) - - areaCell = dsRestart[areaVarName] - print(f'shape(areaCell) = {numpy.shape(areaCell.values)}') - - # for convenience, rename the variables to simpler, shorter names - ds = ds.rename(self.variableDict) - - for varName in variableList: - print(f'multiply {varName} by area') - varAreaName = f'{varName}_{areaVarName}' - ds[varAreaName] = ds[varName] / areaCell - if fractionVarName is not None: - frac = ds[fractionVarName] - ds[varAreaName] = ds[varName] * frac - ds[varAreaName].attrs['units'] = 'm^2' - ds[varAreaName].attrs['description'] = \ - f'{varName} multiplied by the cell area' - - return ds - class PlotRegionHistogramSubtask(AnalysisTask): """ Plots a T-S diagram for a given ocean region @@ -536,10 +502,18 @@ def run_task(self): # Use xarray to open climatology dataset print(f'Open {inFileName}') ds = xarray.open_dataset(inFileName) + if config.has_option(self.taskName, 'weightByVariable'): + weightVarName = config.get(self.taskName, 'weightByVariable') + else: + weightVarName = 'areaCell' + weights = [] + restartFileName = self.runStreams.readpath('restart')[0] + dsRestart = xarray.open_dataset(restartFileName) + dsRestart = dsRestart.isel(Time=0) + print(f'nCells before masking: {ds.sizes["nCells"]}') ds = ds.where(cellMask, drop=True) + dsRestart = dsRestart.where(cellMask, drop=True) - #TODO add region specification - #ds.isel(nRegions=self.regionIndex)) if config.has_option(self.taskName, 'lineColors'): lineColors = [config.get(self.taskName, 'mainColor')] else: @@ -589,6 +563,7 @@ def run_task(self): #TODO title as attribute or dict of var varTitle = varname fields = [ds[varname]] + weights.append(dsRestart[weightVarName].values) xLabel = f"{ds[varname].attrs['long_name']} ({ds[varname].attrs['units']})" for obsName in self.obsDicts: localObsDict = dict(self.obsDicts[obsName]) @@ -605,8 +580,9 @@ def run_task(self): lineColors.append(obsColor) if lineWidths is not None: lineWidths.append([lineWidths[0]]) + weights.append(None) histogram_analysis_plot(config, fields, calendar=calendar, - title=title, xlabel=xLabel, ylabel=yLabel, bins=bins, + title=title, xlabel=xLabel, ylabel=yLabel, bins=bins, weights=weights, lineColors=lineColors, lineWidths=lineWidths, legendText=legendText, titleFontSize=titleFontSize, defaultFontSize=defaultFontSize) @@ -628,4 +604,3 @@ def run_task(self): thumbnailDescription=self.regionName.replace('_', ' '), imageDescription=caption, imageCaption=caption) - diff --git a/mpas_analysis/shared/plot/histogram.py b/mpas_analysis/shared/plot/histogram.py index c6bc9a859..8643e470d 100644 --- a/mpas_analysis/shared/plot/histogram.py +++ b/mpas_analysis/shared/plot/histogram.py @@ -29,7 +29,7 @@ from mpas_analysis.shared.plot.title import limit_title def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, - bins=20, range=None, density=True, lineColors=None, + bins=20, range=None, density=True, weights=None, lineColors=None, lineStyles=None, markers=None, lineWidths=None, legendText=None, titleFontSize=None, axisFontSize=None, defaultFontSize=None, @@ -147,8 +147,9 @@ def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, linewidth = lineWidths[dsIndex] hist_values = dsvalue.values.ravel() + weight = weights[dsIndex] hist_type = 'step' - ax.hist(hist_values, range=range, bins=bins, + ax.hist(hist_values, range=range, bins=bins, weights=weight, linestyle=linestyle, linewidth=linewidth, histtype=hist_type, label=label, density=density) if labelCount > 1: From 82c08f794ba032a842a22a0c9fe5447b9f0f412a Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 27 Sep 2022 17:31:54 -0500 Subject: [PATCH 07/26] Change variable dicts to list for histogram subtask --- mpas_analysis/ocean/histogram.py | 59 +++++++++++++------------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 80485942a..95ebb7be9 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -104,13 +104,8 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N 'gridFileName': 'SSH/zos_AVISO_L4_199210-201012_20180710.nc', 'lonVar': 'lon', 'latVar': 'lat', - 'sshVar': 'zos'}} - #variableList = [] - #self.variableDict = {} - #for var in self.variableList: - # key = f'timeMonthly_avg_{var}' - # variableList.append(key) - # self.variableDict[key] = var + 'sshVar': 'zos', + 'pressureAdjustedSSHVar': 'zos'}} for regionGroup in self.regionGroups: groupObsDicts = {} @@ -135,20 +130,18 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N localObsDict['maskTask'] = obsMasksSubtask groupObsDicts[obsName] = localObsDict - for var in self.variableList: - localVarDict = dict({var: f'timeMonthly_avg_{var}'}) - for regionName in regionNames: - sectionName = None - fullSuffix = self.filePrefix - for season in self.seasons: - plotRegionSubtask = PlotRegionHistogramSubtask( - self, regionGroup, regionName, controlConfig, - sectionName, fullSuffix, mpasClimatologyTask, - mpasMasksSubtask, obsMasksSubtask, groupObsDicts, localVarDict, season) - plotRegionSubtask.run_after(mpasMasksSubtask) - plotRegionSubtask.run_after(obsMasksSubtask) - self.add_subtask(plotRegionSubtask) - print(f'Add regional histogram subtask for {var}, {regionName}, {season}') + for regionName in regionNames: + sectionName = None + fullSuffix = self.filePrefix + for season in self.seasons: + plotRegionSubtask = PlotRegionHistogramSubtask( + self, regionGroup, regionName, controlConfig, + sectionName, fullSuffix, mpasClimatologyTask, + mpasMasksSubtask, obsMasksSubtask, groupObsDicts, self.variableList, season) + plotRegionSubtask.run_after(mpasMasksSubtask) + plotRegionSubtask.run_after(obsMasksSubtask) + self.add_subtask(plotRegionSubtask) + print(f'Add regional histogram subtask for {regionName}, {season}') def setup_and_check(self): """ @@ -172,15 +165,9 @@ def setup_and_check(self): config = self.config regionGroups = config.getexpression(self.taskName, 'regionGroups') - variableList = [] - self.variableDict = {} for var in self.variableList: - key = f'timeMonthly_avg_{var}' - variableList.append(key) - self.variableDict[key] = var - - #for var in self.variableList: + variableList.append(f'timeMonthly_avg_{var}') # # Add xml file names for each season # self.xmlFileNames = [] # for season in self.seasons: @@ -330,8 +317,8 @@ class PlotRegionHistogramSubtask(AnalysisTask): obsDicts : dict of dicts Information on the observations to compare against - varDicts : dict of dicts - Information on the variables to plot + varList: list of str + list of variables to plot season : str The season to compute the climatology for @@ -342,7 +329,7 @@ class PlotRegionHistogramSubtask(AnalysisTask): def __init__(self, parentTask, regionGroup, regionName, controlConfig, sectionName, fullSuffix, mpasClimatologyTask, - mpasMasksSubtask, obsMasksSubtask, obsDicts, varDicts, season): + mpasMasksSubtask, obsMasksSubtask, obsDicts, varList, season): """ Construct the analysis task. @@ -404,7 +391,7 @@ def __init__(self, parentTask, regionGroup, regionName, controlConfig, self.mpasMasksSubtask = mpasMasksSubtask self.obsMasksSubtask = obsMasksSubtask self.obsDicts = obsDicts - self.varDicts = varDicts + self.varList = varList self.season = season self.filePrefix = fullSuffix @@ -436,7 +423,7 @@ def setup_and_check(self): super(PlotRegionHistogramSubtask, self).setup_and_check() self.xmlFileNames = [] - for var in self.varDicts: + for var in self.varList: print(f'add xml from subtask: {self.plotsDirectory}/{self.filePrefix}_{var}_{self.regionName}_{self.season}.xml') # Add xml file names for each season self.xmlFileNames.append(f'{self.plotsDirectory}/{self.filePrefix}_{var}_{self.regionName}_{self.season}.xml') @@ -557,11 +544,11 @@ def run_task(self): yLabel = 'normalized Probability Density Function' - for var in self.varDicts: + for var in self.varList: - varname = f'{self.varDicts[var]}' + varname = f'timeMonthly_avg_{var}' #TODO title as attribute or dict of var - varTitle = varname + varTitle = var fields = [ds[varname]] weights.append(dsRestart[weightVarName].values) xLabel = f"{ds[varname].attrs['long_name']} ({ds[varname].attrs['units']})" From f2ac85f5e287fe82e441626064e53b22710bf405 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 27 Sep 2022 17:38:39 -0500 Subject: [PATCH 08/26] Cleanup OceanHistogram --- mpas_analysis/ocean/histogram.py | 126 ++----------------------- mpas_analysis/shared/plot/histogram.py | 3 + 2 files changed, 9 insertions(+), 120 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 95ebb7be9..1f2d6a332 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -168,12 +168,6 @@ def setup_and_check(self): variableList = [] for var in self.variableList: variableList.append(f'timeMonthly_avg_{var}') - # # Add xml file names for each season - # self.xmlFileNames = [] - # for season in self.seasons: - # for regionName in self.regionNames: - # print(f'add xml from main: {self.plotsDirectory}/{self.filePrefix}_{var}_{regionName}_{season}.xml') - # self.xmlFileNames.append(f'{self.plotsDirectory}/{self.filePrefix}_{var}_{regionName}_{season}.xml') # Specify variables and seasons to compute climology over print(f'add climatology variables') @@ -189,105 +183,6 @@ def run_task(self): # ------- # Carolyn Begeman, Adrian Turner, Xylar Asay-Davis - #FIXUP do nothing here? - print(f'run main task') - #self.logger.info("\nPlotting histogram of ocean vars...") - - #config = self.config - #calendar = self.calendar - #seasons = self.seasons - - #mainRunName = config.get('runs', 'mainRunName') - - #baseDirectory = build_config_full_path( - # config, 'output', 'histogramSubdirectory') - #print(f'baseDirectory={baseDirectory}') - #print(f'plotsDirectory={self.plotsDirectory}') - - ## the variable mpasFieldName will be added to mpasClimatologyTask - ## along with the seasons. - #try: - # restartFileName = self.runStreams.readpath('restart')[0] - #except ValueError: - # raise IOError('No MPAS-O restart file found: need at least one' - # ' restart file to plot T-S diagrams') - - #for season in seasons: - # inFileName = get_unmasked_mpas_climatology_file_name( - # config, season, self.componentName, op='avg') - # # Use xarray to open climatology dataset - # ds = xarray.open_dataset(inFileName) - # ds = self._multiply_var_by_area(ds, self.variableList, areaVarName=areaVarName) - - # #TODO add region specification - # #ds.isel(nRegions=self.regionIndex)) - # if config.has_option(self.taskName, 'lineColors'): - # lineColors = [config.get(self.taskName, 'mainColor')] - # else: - # lineColors = None - # lineWidths = [3] - # legendText = [mainRunName] - - # title = mainRunName - # if config.has_option(self.taskName, 'titleFontSize'): - # titleFontSize = config.getint(self.taskName, - # 'titleFontSize') - # else: - # titleFontSize = None - # if config.has_option(self.taskName, 'titleFontSize'): - # axisFontSize = config.getint(self.taskName, - # 'axisFontSize') - # else: - # axisFontSize = None - - # if config.has_option(self.taskName, 'defaultFontSize'): - # defaultFontSize = config.getint(self.taskName, - # 'defaultFontSize') - # else: - # defaultFontSize = None - # if config.has_option(self.taskName, 'bins'): - # bins = config.getint(self.taskName, 'bins') - # else: - # bins = None - - # yLabel = 'normalized Probability Density Function' - - # for var in self.variableList: - - # fields = [ds[f'{var}_{areaVarName}']] - # #Note: if we want to support 3-d variable histograms, we need to add depth masking - - # #TODO add later - # #if plotControl: - # # fields.append(refData.isel(nRegions=self.regionIndex)) - # # lineColors.append(config.get('histogram', 'controlColor')) - # # lineWidths.append(1.2) - # # legendText.append(controlRunName) - # #TODO make title more informative - # xLabel = f"{ds.ssh.attrs['long_name']} ({ds.ssh.attrs['units']})" - - #histogram_analysis_plot(config, fields, calendar=calendar, - # title=title, xlabel=xLabel, ylabel=yLabel, bins=bins, - # lineColors=lineColors, lineWidths=lineWidths, - # legendText=legendText, - # titleFontSize=titleFontSize, defaultFontSize=defaultFontSize) - - #outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{var}_{season}.png' - #savefig(outFileName, config) - - ##TODO should this be in the outer loop instead? - #caption = 'Normalized probability density function for SSH climatologies in the {} Region'.format(title) - #write_image_xml( - # config=config, - # filePrefix=f'{self.filePrefix}_{var}_{season}', - # componentName='Ocean', - # componentSubdirectory='ocean', - # galleryGroup='Histograms', - # groupLink=f'histogram{var}', - # gallery=f'{var} Histogram', - # thumbnailDescription=title, - # imageDescription=caption, - # imageCaption=caption) class PlotRegionHistogramSubtask(AnalysisTask): """ @@ -395,6 +290,7 @@ def __init__(self, parentTask, regionGroup, regionName, controlConfig, self.season = season self.filePrefix = fullSuffix + #TODO #parallelTaskCount = self.config.getint('execute', 'parallelTaskCount') #self.subprocessCount = min(parallelTaskCount, # self.config.getint(self.taskName, @@ -455,15 +351,6 @@ def run_task(self): print(f'baseDirectory={baseDirectory}') print(f'plotsDirectory={self.plotsDirectory}') - # the variable mpasFieldName will be added to mpasClimatologyTask - # along with the seasons. - #try: - # restartFileName = self.runStreams.readpath('restart')[0] - #except ValueError: - # raise IOError('No MPAS-O restart file found: need at least one' - # ' restart file to plot T-S diagrams') - - #regionMaskFile = self.mpasMasksSubtask.geojsonFileName regionMaskFileName = self.mpasMasksSubtask.maskFileName print(f'Open {regionMaskFileName}') @@ -477,6 +364,9 @@ def run_task(self): inFileName = get_unmasked_mpas_climatology_file_name( config, self.season, self.componentName, op='avg') + + #TODO support control run + #TODO: currently does not support len(obsList) > 1 if len(self.obsDicts) > 0: obsRegionMaskFileName = self.obsMasksSubtask.maskFileName @@ -486,8 +376,6 @@ def run_task(self): dsObsMask = dsObsRegionMask.isel(nRegions=regionIndex) obsCellMask = dsObsMask.regionMasks == 1 - # Use xarray to open climatology dataset - print(f'Open {inFileName}') ds = xarray.open_dataset(inFileName) if config.has_option(self.taskName, 'weightByVariable'): weightVarName = config.get(self.taskName, 'weightByVariable') @@ -497,7 +385,6 @@ def run_task(self): restartFileName = self.runStreams.readpath('restart')[0] dsRestart = xarray.open_dataset(restartFileName) dsRestart = dsRestart.isel(Time=0) - print(f'nCells before masking: {ds.sizes["nCells"]}') ds = ds.where(cellMask, drop=True) dsRestart = dsRestart.where(cellMask, drop=True) @@ -517,7 +404,6 @@ def run_task(self): lineWidths = [config.get(self.taskName, 'lineWidths')] else: lineWidths = None - #lineWidths = [3] legendText = [mainRunName] title = mainRunName @@ -547,8 +433,10 @@ def run_task(self): for var in self.varList: varname = f'timeMonthly_avg_{var}' + #TODO title as attribute or dict of var varTitle = var + fields = [ds[varname]] weights.append(dsRestart[weightVarName].values) xLabel = f"{ds[varname].attrs['long_name']} ({ds[varname].attrs['units']})" @@ -574,11 +462,9 @@ def run_task(self): legendText=legendText, titleFontSize=titleFontSize, defaultFontSize=defaultFontSize) - #fixup outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{var}_{self.regionName}_{self.season}.png' savefig(outFileName, config) - #TODO should this be in the outer loop instead? caption = f'Normalized probability density function for SSH climatologies in {self.regionName}' write_image_xml( config=config, diff --git a/mpas_analysis/shared/plot/histogram.py b/mpas_analysis/shared/plot/histogram.py index 8643e470d..8ee81fd6d 100644 --- a/mpas_analysis/shared/plot/histogram.py +++ b/mpas_analysis/shared/plot/histogram.py @@ -63,6 +63,9 @@ def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, density : logical if True, normalize the histogram so that the area under the curve is 1 + weights: list of numpy data arrays or NoneType's of length dsvalues + the weights corresponding to each entry in dsvalues + lineColors, lineStyles, legendText : list of str, optional control line color, style, and corresponding legend text. Default is black, solid line, and no legend. From 063a5492e026946e7043fb3650447242a1bddd72 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Wed, 28 Sep 2022 15:35:26 -0500 Subject: [PATCH 09/26] Support comparison with control run --- mpas_analysis/ocean/histogram.py | 177 ++++++++++++++++++++++++------- 1 file changed, 140 insertions(+), 37 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 1f2d6a332..618d17884 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -17,8 +17,8 @@ from mpas_analysis.shared import AnalysisTask -from mpas_analysis.shared.io import open_mpas_dataset -from mpas_analysis.shared.io.utility import build_config_full_path, build_obs_path, decode_strings +from mpas_analysis.shared.io import open_mpas_dataset, write_netcdf +from mpas_analysis.shared.io.utility import build_config_full_path, build_obs_path, make_directories, decode_strings from mpas_analysis.shared.climatology import compute_climatology, \ get_unmasked_mpas_climatology_file_name @@ -94,9 +94,13 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N self.variableList = config.getexpression(self.taskName, 'variableList') self.filePrefix = f'histogram_{mainRunName}' + baseDirectory = build_config_full_path( + config, 'output', 'histogramSubdirectory') + if not os.path.exists(baseDirectory): + make_directories(baseDirectory) + #TODO test other seasons obsList = config.getexpression(self.taskName, 'obsList') - #TODO add gridName obsDicts = { 'AVISO': { 'suffix': 'AVISO', @@ -113,30 +117,34 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N mpasMasksSubtask = regionMasksTask.add_mask_subtask( regionGroup=regionGroup) regionNames = mpasMasksSubtask.expand_region_names(self.regionNames) - if controlConfig is None: - for obsName in obsList: - localObsDict = dict(obsDicts[obsName]) - obsFileName = build_obs_path( - config, component=self.componentName, - relativePath=localObsDict['gridFileName']) - print(f'Add mask subtask for {regionGroup}, {obsName}, {localObsDict["gridName"]}') - obsMasksSubtask = regionMasksTask.add_mask_subtask( - regionGroup, obsFileName=obsFileName, - lonVar=localObsDict['lonVar'], - latVar=localObsDict['latVar'], - meshName=localObsDict['gridName']) - regionNames = obsMasksSubtask.expand_region_names(self.regionNames) - obsDicts[obsName]['maskTask'] = obsMasksSubtask - - localObsDict['maskTask'] = obsMasksSubtask - groupObsDicts[obsName] = localObsDict + #TODO support multiple obs + for obsName in obsList: + localObsDict = dict(obsDicts[obsName]) + obsFileName = build_obs_path( + config, component=self.componentName, + relativePath=localObsDict['gridFileName']) + print(f'Add mask subtask for {regionGroup}, {obsName}, {localObsDict["gridName"]}') + obsMasksSubtask = regionMasksTask.add_mask_subtask( + regionGroup, obsFileName=obsFileName, + lonVar=localObsDict['lonVar'], + latVar=localObsDict['latVar'], + meshName=localObsDict['gridName']) + regionNames = obsMasksSubtask.expand_region_names(self.regionNames) + obsDicts[obsName]['maskTask'] = obsMasksSubtask + + localObsDict['maskTask'] = obsMasksSubtask + groupObsDicts[obsName] = localObsDict for regionName in regionNames: sectionName = None - fullSuffix = self.filePrefix + computeWeightsSubtask = ComputeHistogramWeightsSubtask( + self, regionGroup, regionName, mpasMasksSubtask, + self.filePrefix, self.variableList) + self.add_subtask(computeWeightsSubtask) + for season in self.seasons: plotRegionSubtask = PlotRegionHistogramSubtask( self, regionGroup, regionName, controlConfig, - sectionName, fullSuffix, mpasClimatologyTask, + sectionName, self.filePrefix, mpasClimatologyTask, mpasMasksSubtask, obsMasksSubtask, groupObsDicts, self.variableList, season) plotRegionSubtask.run_after(mpasMasksSubtask) plotRegionSubtask.run_after(obsMasksSubtask) @@ -184,9 +192,75 @@ def run_task(self): # Carolyn Begeman, Adrian Turner, Xylar Asay-Davis +class ComputeHistogramWeightsSubtask(AnalysisTask): + """ + fullSuffix : str + The regionGroup and regionName combined and modified to be + appropriate as a task or file suffix + + """ + def __init__(self, parentTask, regionGroup, regionName, mpasMasksSubtask, fullSuffix, varList): + print(f'Initialize weights{fullSuffix} subtask') + super(ComputeHistogramWeightsSubtask, self).__init__( + config=parentTask.config, + taskName=parentTask.taskName, + componentName=parentTask.componentName, + tags=parentTask.tags, + subtaskName=f'weights{fullSuffix}_{regionName}') + + self.mpasMasksSubtask = mpasMasksSubtask + self.regionName = regionName + self.filePrefix = fullSuffix + self.varList = varList + + def setup_and_check(self): + print(f'Setup weights{self.filePrefix} subtask') + super(ComputeHistogramWeightsSubtask, self).setup_and_check() + + def run_task(self): + + print(f'Run weights{self.filePrefix} subtask') + config = self.config + if config.has_option(self.taskName, 'weightByVariable'): + weightVarName = config.get(self.taskName, 'weightByVariable') + else: + weightVarName = 'areaCell' + + #TODO replace plotsDirectory with something more appropriate + baseDirectory = build_config_full_path( + config, 'output', 'histogramSubdirectory') + weightsFileName = f'{baseDirectory}/{self.filePrefix}_{self.regionName}_weights.nc' + self.logger.info(f'weightsFileName is {weightsFileName} in compute') + restartFileName = self.runStreams.readpath('restart')[0] + dsRestart = xarray.open_dataset(restartFileName) + dsRestart = dsRestart.isel(Time=0) + + newRegionMaskFileName = f'{baseDirectory}/{self.filePrefix}_{self.regionName}_mask.nc' + regionMaskFileName = self.mpasMasksSubtask.maskFileName + print(f'Open {regionMaskFileName}') + dsRegionMask = xarray.open_dataset(regionMaskFileName) + maskRegionNames = decode_strings(dsRegionMask.regionNames) + regionIndex = maskRegionNames.index(self.regionName) + dsMask = dsRegionMask.isel(nRegions=regionIndex) + cellMask = dsMask.regionCellMasks == 1 + print(f'Save {newRegionMaskFileName}') + write_netcdf(dsMask, newRegionMaskFileName) + + if os.path.exists(weightsFileName): + self.logger.info(f'{weightsFileName} exists') + return + + dsWeights = xarray.Dataset() + for var in self.varList: + varname = f'timeMonthly_avg_{var}' + dsWeights[f'{varname}_weight'] = dsRestart[weightVarName].where(cellMask, drop=True) + print(f'shape(weightVar) = {numpy.shape(dsRestart[weightVarName].values)}') + print(f'shape(cellMask) = {numpy.shape(cellMask.values)}') + write_netcdf(dsWeights, weightsFileName) + class PlotRegionHistogramSubtask(AnalysisTask): """ - Plots a T-S diagram for a given ocean region + Plots a histogram diagram for a given ocean region Attributes ---------- @@ -365,8 +439,6 @@ def run_task(self): inFileName = get_unmasked_mpas_climatology_file_name( config, self.season, self.componentName, op='avg') - #TODO support control run - #TODO: currently does not support len(obsList) > 1 if len(self.obsDicts) > 0: obsRegionMaskFileName = self.obsMasksSubtask.maskFileName @@ -377,16 +449,30 @@ def run_task(self): dsObsMask = dsObsRegionMask.isel(nRegions=regionIndex) obsCellMask = dsObsMask.regionMasks == 1 ds = xarray.open_dataset(inFileName) - if config.has_option(self.taskName, 'weightByVariable'): - weightVarName = config.get(self.taskName, 'weightByVariable') - else: - weightVarName = 'areaCell' - weights = [] - restartFileName = self.runStreams.readpath('restart')[0] - dsRestart = xarray.open_dataset(restartFileName) - dsRestart = dsRestart.isel(Time=0) ds = ds.where(cellMask, drop=True) - dsRestart = dsRestart.where(cellMask, drop=True) + + baseDirectory = build_config_full_path( + config, 'output', 'histogramSubdirectory') + weightsFileName = f'{baseDirectory}/{self.filePrefix}_{self.regionName}_weights.nc' + print(f'Open weights {weightsFileName}') + dsWeights = xarray.open_dataset(weightsFileName) + + #TODO support control run + if self.controlConfig is not None: + controlRunName = self.controlConfig.get('runs', 'mainRunName') + # TODO + controlFileName = get_unmasked_mpas_climatology_file_name( + self.controlConfig, self.season, self.componentName, op='avg') + print(f'Open control {controlFileName}') + dsControl = xarray.open_dataset(controlFileName) + baseDirectory = build_config_full_path( + self.controlConfig, 'output', 'histogramSubdirectory') + controlWeightsFileName = f'{baseDirectory}/histogram_{controlRunName}_{self.regionName}_weights.nc' + controlRegionMaskFileName = f'{baseDirectory}/histogram_{controlRunName}_{self.regionName}_mask.nc' + print(f'Open control weights {controlWeightsFileName}') + dsControlRegionMasks = xarray.open_dataset(controlRegionMaskFileName) + dsControlWeights = xarray.open_dataset(controlWeightsFileName) + controlCellMask = dsControlRegionMasks.regionCellMasks == 1 if config.has_option(self.taskName, 'lineColors'): lineColors = [config.get(self.taskName, 'mainColor')] @@ -430,6 +516,8 @@ def run_task(self): yLabel = 'normalized Probability Density Function' + fields = [] + weights = [] for var in self.varList: varname = f'timeMonthly_avg_{var}' @@ -437,16 +525,20 @@ def run_task(self): #TODO title as attribute or dict of var varTitle = var - fields = [ds[varname]] - weights.append(dsRestart[weightVarName].values) + fields.append(ds[varname]) + weights.append(dsWeights[f'{varname}_weight'].values) + print(f'Main: {numpy.shape(fields[-1].values)}, {numpy.shape(weights[-1])}') xLabel = f"{ds[varname].attrs['long_name']} ({ds[varname].attrs['units']})" for obsName in self.obsDicts: localObsDict = dict(self.obsDicts[obsName]) obsFileName = build_obs_path( config, component=self.componentName, relativePath=localObsDict['gridFileName']) + #TODO check whether this works + if f'{var}Var' not in localObsDict.keys(): + self.logger.warn(f'{var}Var is not present in {obsName}, skipping {obsName}') + continue varnameObs = localObsDict[f'{var}Var'] - print(f'{var} in obs is {varnameObs}') dsObs = xarray.open_dataset(obsFileName) dsObs = dsObs.where(obsCellMask, drop=True) fields.append(dsObs[varnameObs]) @@ -456,6 +548,17 @@ def run_task(self): if lineWidths is not None: lineWidths.append([lineWidths[0]]) weights.append(None) + if self.controlConfig is not None: + fields.append(dsControl[varname].where(controlCellMask, drop=True)) + controlRunName = self.controlConfig.get('runs', 'mainRunName') + legendText.append('Control') + title = f'{title} vs. {controlRunName}' + if lineColors is not None: + lineColors.append(obsColor) + if lineWidths is not None: + lineWidths.append([lineWidths[0]]) + weights.append(dsControlWeights[f'{varname}_weight'].values) + print(f'Control: {numpy.shape(fields[-1].values)}, {numpy.shape(weights[-1])}') histogram_analysis_plot(config, fields, calendar=calendar, title=title, xlabel=xLabel, ylabel=yLabel, bins=bins, weights=weights, lineColors=lineColors, lineWidths=lineWidths, From 5fda1fa3c3f1bdff53a7312e062576f18152b452 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Wed, 28 Sep 2022 17:22:16 -0500 Subject: [PATCH 10/26] Clean up histogram plot --- mpas_analysis/ocean/histogram.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 618d17884..85d2850cb 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -99,6 +99,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N if not os.path.exists(baseDirectory): make_directories(baseDirectory) + #TODO make sure hist directory purges #TODO test other seasons obsList = config.getexpression(self.taskName, 'obsList') obsDicts = { @@ -177,8 +178,6 @@ def setup_and_check(self): for var in self.variableList: variableList.append(f'timeMonthly_avg_{var}') - # Specify variables and seasons to compute climology over - print(f'add climatology variables') self.mpasClimatologyTask.add_variables(variableList=variableList, seasons=self.seasons) @@ -246,16 +245,14 @@ def run_task(self): print(f'Save {newRegionMaskFileName}') write_netcdf(dsMask, newRegionMaskFileName) - if os.path.exists(weightsFileName): - self.logger.info(f'{weightsFileName} exists') - return - dsWeights = xarray.Dataset() for var in self.varList: varname = f'timeMonthly_avg_{var}' + print(f'save weights for {varname}') dsWeights[f'{varname}_weight'] = dsRestart[weightVarName].where(cellMask, drop=True) print(f'shape(weightVar) = {numpy.shape(dsRestart[weightVarName].values)}') print(f'shape(cellMask) = {numpy.shape(cellMask.values)}') + print(f'Save {weightsFileName}') write_netcdf(dsWeights, weightsFileName) class PlotRegionHistogramSubtask(AnalysisTask): @@ -490,7 +487,6 @@ def run_task(self): lineWidths = [config.get(self.taskName, 'lineWidths')] else: lineWidths = None - legendText = [mainRunName] title = mainRunName if config.has_option(self.taskName, 'titleFontSize'): @@ -518,6 +514,7 @@ def run_task(self): fields = [] weights = [] + legendText = [] for var in self.varList: varname = f'timeMonthly_avg_{var}' @@ -526,7 +523,9 @@ def run_task(self): varTitle = var fields.append(ds[varname]) + print(dsWeights.keys()) weights.append(dsWeights[f'{varname}_weight'].values) + legendText.append(mainRunName) print(f'Main: {numpy.shape(fields[-1].values)}, {numpy.shape(weights[-1])}') xLabel = f"{ds[varname].attrs['long_name']} ({ds[varname].attrs['units']})" for obsName in self.obsDicts: @@ -568,7 +567,7 @@ def run_task(self): outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{var}_{self.regionName}_{self.season}.png' savefig(outFileName, config) - caption = f'Normalized probability density function for SSH climatologies in {self.regionName}' + caption = f'Normalized probability density function for SSH climatologies in {self.regionName.replace("_", " ")}' write_image_xml( config=config, filePrefix=f'{self.filePrefix}_{var}_{self.regionName}_{self.season}', @@ -577,6 +576,6 @@ def run_task(self): galleryGroup=f'{self.regionGroup} Histograms', groupLink=f'histogram{var}', gallery=varTitle, - thumbnailDescription=self.regionName.replace('_', ' '), + thumbnailDescription=f'self.regionName.replace("_", " ") {season}', imageDescription=caption, imageCaption=caption) From 0c2acca7a75813ac33547bdf138fc27587b2daad Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 29 Sep 2022 12:14:08 -0500 Subject: [PATCH 11/26] Purge histogram directory --- mpas_analysis/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mpas_analysis/__main__.py b/mpas_analysis/__main__.py index e958cdbbf..828f50374 100755 --- a/mpas_analysis/__main__.py +++ b/mpas_analysis/__main__.py @@ -717,7 +717,8 @@ def purge_output(config): 'No purge necessary.'.format(outputDirectory)) else: for subdirectory in ['plots', 'logs', 'mpasClimatology', 'mapping', - 'timeSeries', 'html', 'mask', 'profiles']: + 'timeSeries', 'html', 'mask', 'profiles', + 'histogram']: option = '{}Subdirectory'.format(subdirectory) directory = build_config_full_path( config=config, section='output', From d32b921f36afa59c8c2f6e8f9cd320dfe29c0ba3 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 29 Sep 2022 12:48:05 -0500 Subject: [PATCH 12/26] Aesthetic changes to OceanHistogram --- mpas_analysis/ocean/histogram.py | 149 +++++++++++++------------ mpas_analysis/shared/plot/histogram.py | 17 ++- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 85d2850cb..3860de6fc 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -18,7 +18,8 @@ from mpas_analysis.shared import AnalysisTask from mpas_analysis.shared.io import open_mpas_dataset, write_netcdf -from mpas_analysis.shared.io.utility import build_config_full_path, build_obs_path, make_directories, decode_strings +from mpas_analysis.shared.io.utility import build_config_full_path, \ + build_obs_path, make_directories, decode_strings from mpas_analysis.shared.climatology import compute_climatology, \ get_unmasked_mpas_climatology_file_name @@ -26,6 +27,7 @@ from mpas_analysis.shared.plot import histogram_analysis_plot, savefig from mpas_analysis.shared.html import write_image_xml + class OceanHistogram(AnalysisTask): """ Plots a histogram of a 2-d ocean variable. @@ -49,7 +51,8 @@ class OceanHistogram(AnalysisTask): # ------- # Xylar Asay-Davis - def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=None): + def __init__(self, config, mpasClimatologyTask, regionMasksTask, + controlConfig=None): """ Construct the analysis task. @@ -99,8 +102,6 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N if not os.path.exists(baseDirectory): make_directories(baseDirectory) - #TODO make sure hist directory purges - #TODO test other seasons obsList = config.getexpression(self.taskName, 'obsList') obsDicts = { 'AVISO': { @@ -114,43 +115,49 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=N for regionGroup in self.regionGroups: groupObsDicts = {} - print(f'Add mask subtask for {regionGroup}, MPAS') mpasMasksSubtask = regionMasksTask.add_mask_subtask( regionGroup=regionGroup) - regionNames = mpasMasksSubtask.expand_region_names(self.regionNames) - #TODO support multiple obs + regionNames = mpasMasksSubtask.expand_region_names( + self.regionNames) + + # Add mask subtasks for observations and prep groupObsDicts + # groupObsDicts is a subsetted version of localObsDicts with an + # additional attribute for the maskTask for obsName in obsList: localObsDict = dict(obsDicts[obsName]) obsFileName = build_obs_path( config, component=self.componentName, relativePath=localObsDict['gridFileName']) - print(f'Add mask subtask for {regionGroup}, {obsName}, {localObsDict["gridName"]}') obsMasksSubtask = regionMasksTask.add_mask_subtask( regionGroup, obsFileName=obsFileName, lonVar=localObsDict['lonVar'], latVar=localObsDict['latVar'], meshName=localObsDict['gridName']) - regionNames = obsMasksSubtask.expand_region_names(self.regionNames) - obsDicts[obsName]['maskTask'] = obsMasksSubtask - localObsDict['maskTask'] = obsMasksSubtask groupObsDicts[obsName] = localObsDict + for regionName in regionNames: sectionName = None + + # Compute weights for histogram + # TODO make sure that we're not doing unnecessary work when no + # weights are used computeWeightsSubtask = ComputeHistogramWeightsSubtask( self, regionGroup, regionName, mpasMasksSubtask, self.filePrefix, self.variableList) self.add_subtask(computeWeightsSubtask) for season in self.seasons: + + # Generate histogram plots plotRegionSubtask = PlotRegionHistogramSubtask( self, regionGroup, regionName, controlConfig, sectionName, self.filePrefix, mpasClimatologyTask, - mpasMasksSubtask, obsMasksSubtask, groupObsDicts, self.variableList, season) + mpasMasksSubtask, obsMasksSubtask, groupObsDicts, + self.variableList, season) plotRegionSubtask.run_after(mpasMasksSubtask) plotRegionSubtask.run_after(obsMasksSubtask) self.add_subtask(plotRegionSubtask) - print(f'Add regional histogram subtask for {regionName}, {season}') def setup_and_check(self): """ @@ -161,19 +168,13 @@ def setup_and_check(self): OSError If files are not present """ - # Authors - # ------- - # Xylar Asay-Davis - # first, call setup_and_check from the base class (AnalysisTask), # which will perform some common setup, including storing: # self.inDirectory, self.plotsDirectory, self.namelist, self.streams # self.calendar super().setup_and_check() - config = self.config - - regionGroups = config.getexpression(self.taskName, 'regionGroups') + # Add variables and seasons to climatology task variableList = [] for var in self.variableList: variableList.append(f'timeMonthly_avg_{var}') @@ -181,25 +182,23 @@ def setup_and_check(self): self.mpasClimatologyTask.add_variables(variableList=variableList, seasons=self.seasons) - def run_task(self): """ Performs histogram analysis of the output of variables in variableList. """ - # Authors - # ------- - # Carolyn Begeman, Adrian Turner, Xylar Asay-Davis - + # Nothing to do here class ComputeHistogramWeightsSubtask(AnalysisTask): """ + Fetches weight variables from MPAS output files for each variable in variableList. fullSuffix : str The regionGroup and regionName combined and modified to be appropriate as a task or file suffix """ - def __init__(self, parentTask, regionGroup, regionName, mpasMasksSubtask, fullSuffix, varList): - print(f'Initialize weights{fullSuffix} subtask') + def __init__(self, parentTask, regionGroup, regionName, mpasMasksSubtask, + fullSuffix, varList): + super(ComputeHistogramWeightsSubtask, self).__init__( config=parentTask.config, taskName=parentTask.taskName, @@ -213,30 +212,29 @@ def __init__(self, parentTask, regionGroup, regionName, mpasMasksSubtask, fullSu self.varList = varList def setup_and_check(self): - print(f'Setup weights{self.filePrefix} subtask') + super(ComputeHistogramWeightsSubtask, self).setup_and_check() def run_task(self): - print(f'Run weights{self.filePrefix} subtask') config = self.config + # TODO move this check earlier. If unspecified, no weight if config.has_option(self.taskName, 'weightByVariable'): weightVarName = config.get(self.taskName, 'weightByVariable') else: - weightVarName = 'areaCell' + weightVarName = None - #TODO replace plotsDirectory with something more appropriate baseDirectory = build_config_full_path( config, 'output', 'histogramSubdirectory') - weightsFileName = f'{baseDirectory}/{self.filePrefix}_{self.regionName}_weights.nc' - self.logger.info(f'weightsFileName is {weightsFileName} in compute') + weightsFileName = \ + f'{baseDirectory}/{self.filePrefix}_{self.regionName}_weights.nc' restartFileName = self.runStreams.readpath('restart')[0] dsRestart = xarray.open_dataset(restartFileName) dsRestart = dsRestart.isel(Time=0) - newRegionMaskFileName = f'{baseDirectory}/{self.filePrefix}_{self.regionName}_mask.nc' + newRegionMaskFileName = \ + f'{baseDirectory}/{self.filePrefix}_{self.regionName}_mask.nc' regionMaskFileName = self.mpasMasksSubtask.maskFileName - print(f'Open {regionMaskFileName}') dsRegionMask = xarray.open_dataset(regionMaskFileName) maskRegionNames = decode_strings(dsRegionMask.regionNames) regionIndex = maskRegionNames.index(self.regionName) @@ -248,13 +246,11 @@ def run_task(self): dsWeights = xarray.Dataset() for var in self.varList: varname = f'timeMonthly_avg_{var}' - print(f'save weights for {varname}') - dsWeights[f'{varname}_weight'] = dsRestart[weightVarName].where(cellMask, drop=True) - print(f'shape(weightVar) = {numpy.shape(dsRestart[weightVarName].values)}') - print(f'shape(cellMask) = {numpy.shape(cellMask.values)}') - print(f'Save {weightsFileName}') + dsWeights[f'{varname}_weight'] = dsRestart[weightVarName].where( + cellMask, drop=True) write_netcdf(dsWeights, weightsFileName) + class PlotRegionHistogramSubtask(AnalysisTask): """ Plots a histogram diagram for a given ocean region @@ -391,11 +387,9 @@ def setup_and_check(self): self.xmlFileNames = [] for var in self.varList: - print(f'add xml from subtask: {self.plotsDirectory}/{self.filePrefix}_{var}_{self.regionName}_{self.season}.xml') - # Add xml file names for each season - self.xmlFileNames.append(f'{self.plotsDirectory}/{self.filePrefix}_{var}_{self.regionName}_{self.season}.xml') - - print(f'end subtask setup and check') + self.xmlFileNames.append( + f'{self.plotsDirectory}/{self.filePrefix}_{var}_' + f'{self.regionName}_{self.season}.xml') def run_task(self): """ @@ -419,11 +413,8 @@ def run_task(self): baseDirectory = build_config_full_path( config, 'output', 'histogramSubdirectory') - print(f'baseDirectory={baseDirectory}') - print(f'plotsDirectory={self.plotsDirectory}') regionMaskFileName = self.mpasMasksSubtask.maskFileName - print(f'Open {regionMaskFileName}') dsRegionMask = xarray.open_dataset(regionMaskFileName) @@ -450,24 +441,25 @@ def run_task(self): baseDirectory = build_config_full_path( config, 'output', 'histogramSubdirectory') - weightsFileName = f'{baseDirectory}/{self.filePrefix}_{self.regionName}_weights.nc' - print(f'Open weights {weightsFileName}') + weightsFileName = \ + f'{baseDirectory}/{self.filePrefix}_{self.regionName}_' \ + 'weights.nc' dsWeights = xarray.open_dataset(weightsFileName) - #TODO support control run if self.controlConfig is not None: controlRunName = self.controlConfig.get('runs', 'mainRunName') - # TODO controlFileName = get_unmasked_mpas_climatology_file_name( self.controlConfig, self.season, self.componentName, op='avg') - print(f'Open control {controlFileName}') dsControl = xarray.open_dataset(controlFileName) baseDirectory = build_config_full_path( self.controlConfig, 'output', 'histogramSubdirectory') - controlWeightsFileName = f'{baseDirectory}/histogram_{controlRunName}_{self.regionName}_weights.nc' - controlRegionMaskFileName = f'{baseDirectory}/histogram_{controlRunName}_{self.regionName}_mask.nc' - print(f'Open control weights {controlWeightsFileName}') - dsControlRegionMasks = xarray.open_dataset(controlRegionMaskFileName) + controlWeightsFileName = \ + f'{baseDirectory}/histogram_{controlRunName}_' \ + f'{self.regionName}_weights.nc' + controlRegionMaskFileName = f'{baseDirectory}/histogram_' \ + f'{controlRunName}_{self.regionName}_mask.nc' + dsControlRegionMasks = xarray.open_dataset( + controlRegionMaskFileName) dsControlWeights = xarray.open_dataset(controlWeightsFileName) controlCellMask = dsControlRegionMasks.regionCellMasks == 1 @@ -512,30 +504,31 @@ def run_task(self): yLabel = 'normalized Probability Density Function' - fields = [] - weights = [] - legendText = [] for var in self.varList: + fields = [] + weights = [] + legendText = [] + varname = f'timeMonthly_avg_{var}' #TODO title as attribute or dict of var varTitle = var fields.append(ds[varname]) - print(dsWeights.keys()) weights.append(dsWeights[f'{varname}_weight'].values) legendText.append(mainRunName) - print(f'Main: {numpy.shape(fields[-1].values)}, {numpy.shape(weights[-1])}') - xLabel = f"{ds[varname].attrs['long_name']} ({ds[varname].attrs['units']})" + xLabel = f"{ds[varname].attrs['long_name']} " \ + f"({ds[varname].attrs['units']})" for obsName in self.obsDicts: localObsDict = dict(self.obsDicts[obsName]) obsFileName = build_obs_path( config, component=self.componentName, relativePath=localObsDict['gridFileName']) - #TODO check whether this works if f'{var}Var' not in localObsDict.keys(): - self.logger.warn(f'{var}Var is not present in {obsName}, skipping {obsName}') + self.logger.warn( + f'{var}Var is not present in {obsName}, skipping ' + f'{obsName}') continue varnameObs = localObsDict[f'{var}Var'] dsObs = xarray.open_dataset(obsFileName) @@ -548,7 +541,8 @@ def run_task(self): lineWidths.append([lineWidths[0]]) weights.append(None) if self.controlConfig is not None: - fields.append(dsControl[varname].where(controlCellMask, drop=True)) + fields.append(dsControl[varname].where(controlCellMask, + drop=True)) controlRunName = self.controlConfig.get('runs', 'mainRunName') legendText.append('Control') title = f'{title} vs. {controlRunName}' @@ -557,25 +551,32 @@ def run_task(self): if lineWidths is not None: lineWidths.append([lineWidths[0]]) weights.append(dsControlWeights[f'{varname}_weight'].values) - print(f'Control: {numpy.shape(fields[-1].values)}, {numpy.shape(weights[-1])}') histogram_analysis_plot(config, fields, calendar=calendar, - title=title, xlabel=xLabel, ylabel=yLabel, bins=bins, weights=weights, - lineColors=lineColors, lineWidths=lineWidths, + title=title, xlabel=xLabel, ylabel=yLabel, + bins=bins, weights=weights, + lineColors=lineColors, + lineWidths=lineWidths, legendText=legendText, - titleFontSize=titleFontSize, defaultFontSize=defaultFontSize) + titleFontSize=titleFontSize, + defaultFontSize=defaultFontSize) - outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{var}_{self.regionName}_{self.season}.png' + outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{var}_' \ + f'{self.regionName}_{self.season}.png' savefig(outFileName, config) - caption = f'Normalized probability density function for SSH climatologies in {self.regionName.replace("_", " ")}' + caption = f'Normalized probability density function for SSH ' \ + f'climatologies in {self.regionName.replace("_", " ")}' + write_image_xml( config=config, - filePrefix=f'{self.filePrefix}_{var}_{self.regionName}_{self.season}', + filePrefix=f'{self.filePrefix}_{var}_{self.regionName}_' + f'{self.season}', componentName='Ocean', componentSubdirectory='ocean', galleryGroup=f'{self.regionGroup} Histograms', groupLink=f'histogram{var}', gallery=varTitle, - thumbnailDescription=f'self.regionName.replace("_", " ") {season}', + thumbnailDescription=f'{self.regionName.replace("_", " ")} ' + f'{self.season}', imageDescription=caption, imageCaption=caption) diff --git a/mpas_analysis/shared/plot/histogram.py b/mpas_analysis/shared/plot/histogram.py index 8ee81fd6d..8ec57c1ce 100644 --- a/mpas_analysis/shared/plot/histogram.py +++ b/mpas_analysis/shared/plot/histogram.py @@ -28,14 +28,14 @@ from mpas_analysis.shared.plot.ticks import plot_xtick_format from mpas_analysis.shared.plot.title import limit_title + def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, - bins=20, range=None, density=True, weights=None, lineColors=None, - lineStyles=None, markers=None, lineWidths=None, - legendText=None, - titleFontSize=None, axisFontSize=None, defaultFontSize=None, - figsize=(12, 6), dpi=None, - legendLocation='upper right', - maxTitleLength=90): + bins=20, range=None, density=True, weights=None, + lineColors=None, lineStyles=None, markers=None, + lineWidths=None, legendText=None, + titleFontSize=None, axisFontSize=None, + defaultFontSize=None, figsize=(12, 6), dpi=None, + legendLocation='upper right', maxTitleLength=90): """ Plots the list of histogram data sets. @@ -47,8 +47,7 @@ def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, control plotting dsvalues : list of xarray DataSets - the data set(s) to be plotted. For area or volume quantities, multiply by the - respective area or volume before inputting. Datasets should already be sliced + the data set(s) to be plotted. Datasets should already be sliced within the time range specified in the config file. title : str From 3121c17ca05fb5bce46e3f8b341803c7aee47b5a Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 29 Sep 2022 13:30:20 -0500 Subject: [PATCH 13/26] Replace weightByVariable by weightList --- mpas_analysis/ocean/histogram.py | 97 +++++++++++++++++++------------- 1 file changed, 59 insertions(+), 38 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 3860de6fc..3b46ee476 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -95,6 +95,11 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, self.regionNames = config.getexpression(self.taskName, 'regionNames') self.seasons = config.getexpression(self.taskName, 'seasons') self.variableList = config.getexpression(self.taskName, 'variableList') + if config.has_option(self.taskName, 'weightList'): + self.weightList = config.getexpression(self.taskName, 'weightList') + else: + self.weightList = None + self.filePrefix = f'histogram_{mainRunName}' baseDirectory = build_config_full_path( @@ -140,12 +145,11 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, sectionName = None # Compute weights for histogram - # TODO make sure that we're not doing unnecessary work when no - # weights are used - computeWeightsSubtask = ComputeHistogramWeightsSubtask( - self, regionGroup, regionName, mpasMasksSubtask, - self.filePrefix, self.variableList) - self.add_subtask(computeWeightsSubtask) + if self.weightList is not None: + computeWeightsSubtask = ComputeHistogramWeightsSubtask( + self, regionGroup, regionName, mpasMasksSubtask, + self.filePrefix, self.variableList, self.weightList) + self.add_subtask(computeWeightsSubtask) for season in self.seasons: @@ -154,7 +158,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, self, regionGroup, regionName, controlConfig, sectionName, self.filePrefix, mpasClimatologyTask, mpasMasksSubtask, obsMasksSubtask, groupObsDicts, - self.variableList, season) + self.variableList, self.weightList, season) plotRegionSubtask.run_after(mpasMasksSubtask) plotRegionSubtask.run_after(obsMasksSubtask) self.add_subtask(plotRegionSubtask) @@ -182,6 +186,11 @@ def setup_and_check(self): self.mpasClimatologyTask.add_variables(variableList=variableList, seasons=self.seasons) + if self.weightList is not None: + if len(self.weightList) != len(self.variableList): + self.logger.error('Histogram weightList is not the same ' + 'length as variableList') + def run_task(self): """ Performs histogram analysis of the output of variables in variableList. @@ -197,7 +206,7 @@ class ComputeHistogramWeightsSubtask(AnalysisTask): """ def __init__(self, parentTask, regionGroup, regionName, mpasMasksSubtask, - fullSuffix, varList): + fullSuffix, varList, weightList): super(ComputeHistogramWeightsSubtask, self).__init__( config=parentTask.config, @@ -210,6 +219,7 @@ def __init__(self, parentTask, regionGroup, regionName, mpasMasksSubtask, self.regionName = regionName self.filePrefix = fullSuffix self.varList = varList + self.weightList = weightList def setup_and_check(self): @@ -218,11 +228,6 @@ def setup_and_check(self): def run_task(self): config = self.config - # TODO move this check earlier. If unspecified, no weight - if config.has_option(self.taskName, 'weightByVariable'): - weightVarName = config.get(self.taskName, 'weightByVariable') - else: - weightVarName = None baseDirectory = build_config_full_path( config, 'output', 'histogramSubdirectory') @@ -244,10 +249,15 @@ def run_task(self): write_netcdf(dsMask, newRegionMaskFileName) dsWeights = xarray.Dataset() - for var in self.varList: - varname = f'timeMonthly_avg_{var}' - dsWeights[f'{varname}_weight'] = dsRestart[weightVarName].where( - cellMask, drop=True) + for index, var in enumerate(self.varList): + weightVarName = self.weightList[index] + if weightVarName in dsRestart.keys(): + varname = f'timeMonthly_avg_{var}' + dsWeights[f'{varname}_weight'] = dsRestart[weightVarName].where( + cellMask, drop=True) + else: + self.logger.warn(f'Weight variable {weightVarName} is not in ' + f'the restart file, skipping') write_netcdf(dsWeights, weightsFileName) @@ -290,8 +300,8 @@ class PlotRegionHistogramSubtask(AnalysisTask): # Xylar Asay-Davis def __init__(self, parentTask, regionGroup, regionName, controlConfig, - sectionName, fullSuffix, mpasClimatologyTask, - mpasMasksSubtask, obsMasksSubtask, obsDicts, varList, season): + sectionName, fullSuffix, mpasClimatologyTask, mpasMasksSubtask, + obsMasksSubtask, obsDicts, varList, weightList, season): """ Construct the analysis task. @@ -353,7 +363,8 @@ def __init__(self, parentTask, regionGroup, regionName, controlConfig, self.mpasMasksSubtask = mpasMasksSubtask self.obsMasksSubtask = obsMasksSubtask self.obsDicts = obsDicts - self.varList = varList + self.variableList = varList + self.weightList = weightList self.season = season self.filePrefix = fullSuffix @@ -386,7 +397,7 @@ def setup_and_check(self): super(PlotRegionHistogramSubtask, self).setup_and_check() self.xmlFileNames = [] - for var in self.varList: + for var in self.variableList: self.xmlFileNames.append( f'{self.plotsDirectory}/{self.filePrefix}_{var}_' f'{self.regionName}_{self.season}.xml') @@ -399,14 +410,12 @@ def run_task(self): # ------- # Xylar Asay-Davis - self.logger.info("\nPlotting histogram for {}" - "...".format(self.regionName)) + self.logger.info(f"\nPlotting {self.season} histograms for " + f"{self.regionName}") config = self.config sectionName = self.sectionName - self.logger.info(' Make plots...') - calendar = self.calendar mainRunName = config.get('runs', 'mainRunName') @@ -441,10 +450,11 @@ def run_task(self): baseDirectory = build_config_full_path( config, 'output', 'histogramSubdirectory') - weightsFileName = \ - f'{baseDirectory}/{self.filePrefix}_{self.regionName}_' \ - 'weights.nc' - dsWeights = xarray.open_dataset(weightsFileName) + if self.weightList is not None: + weightsFileName = \ + f'{baseDirectory}/{self.filePrefix}_{self.regionName}_' \ + 'weights.nc' + dsWeights = xarray.open_dataset(weightsFileName) if self.controlConfig is not None: controlRunName = self.controlConfig.get('runs', 'mainRunName') @@ -453,15 +463,16 @@ def run_task(self): dsControl = xarray.open_dataset(controlFileName) baseDirectory = build_config_full_path( self.controlConfig, 'output', 'histogramSubdirectory') - controlWeightsFileName = \ - f'{baseDirectory}/histogram_{controlRunName}_' \ - f'{self.regionName}_weights.nc' controlRegionMaskFileName = f'{baseDirectory}/histogram_' \ f'{controlRunName}_{self.regionName}_mask.nc' dsControlRegionMasks = xarray.open_dataset( controlRegionMaskFileName) - dsControlWeights = xarray.open_dataset(controlWeightsFileName) controlCellMask = dsControlRegionMasks.regionCellMasks == 1 + if self.weightList is not None: + controlWeightsFileName = \ + f'{baseDirectory}/histogram_{controlRunName}_' \ + f'{self.regionName}_weights.nc' + dsControlWeights = xarray.open_dataset(controlWeightsFileName) if config.has_option(self.taskName, 'lineColors'): lineColors = [config.get(self.taskName, 'mainColor')] @@ -504,7 +515,7 @@ def run_task(self): yLabel = 'normalized Probability Density Function' - for var in self.varList: + for index, var in enumerate(self.variableList): fields = [] weights = [] @@ -512,14 +523,27 @@ def run_task(self): varname = f'timeMonthly_avg_{var}' + caption = f'Normalized probability density function for {var} ' \ + f'climatologies in {self.regionName.replace("_", " ")}' + #TODO title as attribute or dict of var varTitle = var fields.append(ds[varname]) - weights.append(dsWeights[f'{varname}_weight'].values) + if self.weightList is not None: + if f'{varname}_weight' in dsWeights.keys(): + weights.append(dsWeights[f'{varname}_weight'].values) + caption = f'{caption} weighted by {self.weightList[index]}' + else: + weights.append(None) + else: + weights.append(None) + legendText.append(mainRunName) + xLabel = f"{ds[varname].attrs['long_name']} " \ f"({ds[varname].attrs['units']})" + for obsName in self.obsDicts: localObsDict = dict(self.obsDicts[obsName]) obsFileName = build_obs_path( @@ -564,9 +588,6 @@ def run_task(self): f'{self.regionName}_{self.season}.png' savefig(outFileName, config) - caption = f'Normalized probability density function for SSH ' \ - f'climatologies in {self.regionName.replace("_", " ")}' - write_image_xml( config=config, filePrefix=f'{self.filePrefix}_{var}_{self.regionName}_' From 7266c6de1917ce865c51ad9e1e77b33a971ca0d6 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Thu, 29 Sep 2022 16:35:28 -0500 Subject: [PATCH 14/26] Aesthetic changes to OceanHistogram --- mpas_analysis/ocean/histogram.py | 314 ++++++++++++------------- mpas_analysis/shared/plot/histogram.py | 46 ++-- 2 files changed, 173 insertions(+), 187 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 3b46ee476..64955e8a3 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -32,24 +32,7 @@ class OceanHistogram(AnalysisTask): """ Plots a histogram of a 2-d ocean variable. - Attributes - ---------- - variableDict : dict - A dictionary of variables from the time series stats monthly output - (keys), together with shorter, more convenient names (values) - - histogramFileName : str - The name of the file where the histogram is stored - - controlConfig : mpas_tools.config.MpasConfigParser - Configuration options for a control run (if one is provided) - - filePrefix : str - The basename (without extension) of the PNG and XML files to write out """ - # Authors - # ------- - # Xylar Asay-Davis def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig=None): @@ -62,9 +45,6 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, config : mpas_tools.config.MpasConfigParser Configuration options - mpasHistogram: ``MpasHistogramTask`` - The task that extracts the time series from MPAS monthly output - mpasClimatologyTask : ``MpasClimatologyTask`` The task that produced the climatology to be remapped and plotted @@ -74,9 +54,6 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, controlConfig : mpas_tools.config.MpasConfigParser Configuration options for a control run (if any) """ - # Authors - # ------- - # Xylar Asay-Davis # first, call the constructor from the base class (AnalysisTask) super().__init__( @@ -107,7 +84,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, if not os.path.exists(baseDirectory): make_directories(baseDirectory) - obsList = config.getexpression(self.taskName, 'obsList') + self.obsList = config.getexpression(self.taskName, 'obsList') obsDicts = { 'AVISO': { 'suffix': 'AVISO', @@ -128,7 +105,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, # Add mask subtasks for observations and prep groupObsDicts # groupObsDicts is a subsetted version of localObsDicts with an # additional attribute for the maskTask - for obsName in obsList: + for obsName in self.obsList: localObsDict = dict(obsDicts[obsName]) obsFileName = build_obs_path( config, component=self.componentName, @@ -147,8 +124,8 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, # Compute weights for histogram if self.weightList is not None: computeWeightsSubtask = ComputeHistogramWeightsSubtask( - self, regionGroup, regionName, mpasMasksSubtask, - self.filePrefix, self.variableList, self.weightList) + self, regionName, mpasMasksSubtask, self.filePrefix, + self.variableList, self.weightList) self.add_subtask(computeWeightsSubtask) for season in self.seasons: @@ -188,25 +165,52 @@ def setup_and_check(self): if self.weightList is not None: if len(self.weightList) != len(self.variableList): - self.logger.error('Histogram weightList is not the same ' - 'length as variableList') + raise ValueError('Histogram weightList is not the same ' + 'length as variableList') + if len(self.obsList) > 1: + raise ValueError('Histogram analysis does not currently support' + 'more than one observational product') - def run_task(self): - """ - Performs histogram analysis of the output of variables in variableList. - """ - # Nothing to do here class ComputeHistogramWeightsSubtask(AnalysisTask): """ - Fetches weight variables from MPAS output files for each variable in variableList. + Fetches weight variables from MPAS output files for each variable in + variableList. + + """ + def __init__(self, parentTask, regionName, mpasMasksSubtask, fullSuffix, + variableList, weightList): + """ + Initialize weights task + + Parameters + ---------- + parentTask : ``AnalysisTask`` + The parent task, used to get the ``taskName``, ``config`` and + ``componentName`` + + regionName : str + Name of the region to plot + + mpasMasksSubtask : ``ComputeRegionMasksSubtask`` + A task for creating mask MPAS files for each region to plot, used + to get the mask file name + + obsDicts : dict of dicts + Information on the observations to compare agains + fullSuffix : str The regionGroup and regionName combined and modified to be appropriate as a task or file suffix - """ - def __init__(self, parentTask, regionGroup, regionName, mpasMasksSubtask, - fullSuffix, varList, weightList): + variableList: list of str + List of variables which will be weighted + + weightList: list of str + List of variables by which to weight the variables in + variableList, of the same length as variableList + + """ super(ComputeHistogramWeightsSubtask, self).__init__( config=parentTask.config, @@ -218,47 +222,53 @@ def __init__(self, parentTask, regionGroup, regionName, mpasMasksSubtask, self.mpasMasksSubtask = mpasMasksSubtask self.regionName = regionName self.filePrefix = fullSuffix - self.varList = varList + self.variableList = variableList self.weightList = weightList - def setup_and_check(self): - - super(ComputeHistogramWeightsSubtask, self).setup_and_check() - def run_task(self): + """ + Apply the region mask to each weight variable and save in a file common + to that region. + """ config = self.config - - baseDirectory = build_config_full_path( + base_directory = build_config_full_path( config, 'output', 'histogramSubdirectory') - weightsFileName = \ - f'{baseDirectory}/{self.filePrefix}_{self.regionName}_weights.nc' - restartFileName = self.runStreams.readpath('restart')[0] - dsRestart = xarray.open_dataset(restartFileName) - dsRestart = dsRestart.isel(Time=0) - - newRegionMaskFileName = \ - f'{baseDirectory}/{self.filePrefix}_{self.regionName}_mask.nc' - regionMaskFileName = self.mpasMasksSubtask.maskFileName - dsRegionMask = xarray.open_dataset(regionMaskFileName) - maskRegionNames = decode_strings(dsRegionMask.regionNames) - regionIndex = maskRegionNames.index(self.regionName) - dsMask = dsRegionMask.isel(nRegions=regionIndex) - cellMask = dsMask.regionCellMasks == 1 - print(f'Save {newRegionMaskFileName}') - write_netcdf(dsMask, newRegionMaskFileName) - - dsWeights = xarray.Dataset() - for index, var in enumerate(self.varList): - weightVarName = self.weightList[index] - if weightVarName in dsRestart.keys(): - varname = f'timeMonthly_avg_{var}' - dsWeights[f'{varname}_weight'] = dsRestart[weightVarName].where( - cellMask, drop=True) + + # Get cell mask for the region + region_mask_filename = self.mpasMasksSubtask.maskFileName + ds_region_mask = xarray.open_dataset(region_mask_filename) + mask_region_names = decode_strings(ds_region_mask.regionNames) + region_index = mask_region_names.index(self.regionName) + 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) + + # 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) + new_region_mask_filename = \ + f'{base_directory}/{self.filePrefix}_{self.regionName}_mask.nc' + write_netcdf(ds_mask, new_region_mask_filename) + + ds_weights = xarray.Dataset() + # 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(): + var_name = f'timeMonthly_avg_{var}' + ds_weights[f'{var_name}_weight'] = \ + ds_restart[weight_var_name].where(cell_mask, drop=True) else: - self.logger.warn(f'Weight variable {weightVarName} is not in ' + self.logger.warn(f'Weight variable {weight_var_name} is not in ' f'the restart file, skipping') - write_netcdf(dsWeights, weightsFileName) + + weights_filename = \ + f'{base_directory}/{self.filePrefix}_{self.regionName}_weights.nc' + write_netcdf(ds_weights, weights_filename) class PlotRegionHistogramSubtask(AnalysisTask): @@ -289,19 +299,17 @@ class PlotRegionHistogramSubtask(AnalysisTask): obsDicts : dict of dicts Information on the observations to compare against - varList: list of str + variableList: list of str list of variables to plot season : str The season to compute the climatology for """ - # Authors - # ------- - # Xylar Asay-Davis def __init__(self, parentTask, regionGroup, regionName, controlConfig, - sectionName, fullSuffix, mpasClimatologyTask, mpasMasksSubtask, - obsMasksSubtask, obsDicts, varList, weightList, season): + sectionName, fullSuffix, mpasClimatologyTask, + mpasMasksSubtask, obsMasksSubtask, obsDicts, variableList, + weightList, season): """ Construct the analysis task. @@ -341,12 +349,8 @@ def __init__(self, parentTask, regionGroup, regionName, controlConfig, season : str The season to comput the climatogy for """ - # Authors - # ------- - # Xylar Asay-Davis # first, call the constructor from the base class (AnalysisTask) - print(f'Initialize histogram task') super(PlotRegionHistogramSubtask, self).__init__( config=parentTask.config, taskName=parentTask.taskName, @@ -363,20 +367,11 @@ def __init__(self, parentTask, regionGroup, regionName, controlConfig, self.mpasMasksSubtask = mpasMasksSubtask self.obsMasksSubtask = obsMasksSubtask self.obsDicts = obsDicts - self.variableList = varList + self.variableList = variableList self.weightList = weightList self.season = season self.filePrefix = fullSuffix - #TODO - #parallelTaskCount = self.config.getint('execute', 'parallelTaskCount') - #self.subprocessCount = min(parallelTaskCount, - # self.config.getint(self.taskName, - # 'subprocessCount')) - #self.daskThreads = min( - # multiprocessing.cpu_count(), - # self.config.getint(self.taskName, 'daskThreads')) - def setup_and_check(self): """ Perform steps to set up the analysis and check for errors in the setup. @@ -386,9 +381,6 @@ def setup_and_check(self): IOError If files are not present """ - # Authors - # ------- - # Xylar Asay-Davis # first, call setup_and_check from the base class (AnalysisTask), # which will perform some common setup, including storing: @@ -404,11 +396,8 @@ def setup_and_check(self): def run_task(self): """ - Plots time-series output of properties in an ocean region. + Plots histograms of properties in an ocean region. """ - # Authors - # ------- - # Xylar Asay-Davis self.logger.info(f"\nPlotting {self.season} histograms for " f"{self.regionName}") @@ -418,61 +407,58 @@ def run_task(self): calendar = self.calendar - mainRunName = config.get('runs', 'mainRunName') - - baseDirectory = build_config_full_path( - config, 'output', 'histogramSubdirectory') - - regionMaskFileName = self.mpasMasksSubtask.maskFileName + main_run_name = config.get('runs', 'mainRunName') - dsRegionMask = xarray.open_dataset(regionMaskFileName) + region_mask_filename = self.mpasMasksSubtask.maskFileName - maskRegionNames = decode_strings(dsRegionMask.regionNames) - regionIndex = maskRegionNames.index(self.regionName) + ds_region_mask = xarray.open_dataset(region_mask_filename) - dsMask = dsRegionMask.isel(nRegions=regionIndex) - cellMask = dsMask.regionCellMasks == 1 + mask_region_names = decode_strings(ds_region_mask.regionNames) + region_index = mask_region_names.index(self.regionName) - inFileName = get_unmasked_mpas_climatology_file_name( - config, self.season, self.componentName, op='avg') + ds_mask = ds_region_mask.isel(nRegions=region_index) + cell_mask = ds_mask.regionCellMasks == 1 - #TODO: currently does not support len(obsList) > 1 if len(self.obsDicts) > 0: - obsRegionMaskFileName = self.obsMasksSubtask.maskFileName - dsObsRegionMask = xarray.open_dataset(obsRegionMaskFileName) - maskRegionNames = decode_strings(dsRegionMask.regionNames) - regionIndex = maskRegionNames.index(self.regionName) + obs_region_mask_filename = self.obsMasksSubtask.maskFileName + ds_obs_region_mask = xarray.open_dataset(obs_region_mask_filename) + mask_region_names = decode_strings(ds_region_mask.regionNames) + region_index = mask_region_names.index(self.regionName) - dsObsMask = dsObsRegionMask.isel(nRegions=regionIndex) - obsCellMask = dsObsMask.regionMasks == 1 - ds = xarray.open_dataset(inFileName) - ds = ds.where(cellMask, drop=True) + ds_obs_mask = ds_obs_region_mask.isel(nRegions=region_index) + obs_cell_mask = ds_obs_mask.regionMasks == 1 - baseDirectory = build_config_full_path( + in_filename = get_unmasked_mpas_climatology_file_name( + config, self.season, self.componentName, op='avg') + ds = xarray.open_dataset(in_filename) + ds = ds.where(cell_mask, drop=True) + + base_directory = build_config_full_path( config, 'output', 'histogramSubdirectory') + if self.weightList is not None: - weightsFileName = \ - f'{baseDirectory}/{self.filePrefix}_{self.regionName}_' \ + weights_filename = \ + f'{base_directory}/{self.filePrefix}_{self.regionName}_' \ 'weights.nc' - dsWeights = xarray.open_dataset(weightsFileName) + ds_weights = xarray.open_dataset(weights_filename) if self.controlConfig is not None: - controlRunName = self.controlConfig.get('runs', 'mainRunName') - controlFileName = get_unmasked_mpas_climatology_file_name( + control_run_name = self.controlConfig.get('runs', 'mainRunName') + control_filename = get_unmasked_mpas_climatology_file_name( self.controlConfig, self.season, self.componentName, op='avg') - dsControl = xarray.open_dataset(controlFileName) - baseDirectory = build_config_full_path( + ds_control = xarray.open_dataset(control_filename) + base_directory = build_config_full_path( self.controlConfig, 'output', 'histogramSubdirectory') - controlRegionMaskFileName = f'{baseDirectory}/histogram_' \ - f'{controlRunName}_{self.regionName}_mask.nc' - dsControlRegionMasks = xarray.open_dataset( - controlRegionMaskFileName) - controlCellMask = dsControlRegionMasks.regionCellMasks == 1 + control_region_mask_filename = f'{base_directory}/histogram_' \ + f'{control_run_name}_{self.regionName}_mask.nc' + ds_control_region_masks = xarray.open_dataset( + control_region_mask_filename) + control_cell_mask = ds_control_region_masks.regionCellMasks == 1 if self.weightList is not None: - controlWeightsFileName = \ - f'{baseDirectory}/histogram_{controlRunName}_' \ + control_weights_filename = \ + f'{base_directory}/histogram_{control_run_name}_' \ f'{self.regionName}_weights.nc' - dsControlWeights = xarray.open_dataset(controlWeightsFileName) + ds_control_weights = xarray.open_dataset(control_weights_filename) if config.has_option(self.taskName, 'lineColors'): lineColors = [config.get(self.taskName, 'mainColor')] @@ -491,7 +477,7 @@ def run_task(self): else: lineWidths = None - title = mainRunName + title = main_run_name if config.has_option(self.taskName, 'titleFontSize'): titleFontSize = config.getint(self.taskName, 'titleFontSize') @@ -521,62 +507,62 @@ def run_task(self): weights = [] legendText = [] - varname = f'timeMonthly_avg_{var}' + var_name = f'timeMonthly_avg_{var}' caption = f'Normalized probability density function for {var} ' \ f'climatologies in {self.regionName.replace("_", " ")}' - #TODO title as attribute or dict of var + # Note: consider modifying this for more professional headings varTitle = var - fields.append(ds[varname]) + fields.append(ds[var_name]) if self.weightList is not None: - if f'{varname}_weight' in dsWeights.keys(): - weights.append(dsWeights[f'{varname}_weight'].values) + if f'{var_name}_weight' in ds_weights.keys(): + weights.append(ds_weights[f'{var_name}_weight'].values) caption = f'{caption} weighted by {self.weightList[index]}' else: weights.append(None) else: weights.append(None) - legendText.append(mainRunName) + legendText.append(main_run_name) - xLabel = f"{ds[varname].attrs['long_name']} " \ - f"({ds[varname].attrs['units']})" + xLabel = f"{ds[var_name].attrs['long_name']} " \ + f"({ds[var_name].attrs['units']})" - for obsName in self.obsDicts: - localObsDict = dict(self.obsDicts[obsName]) - obsFileName = build_obs_path( + for obs_name in self.obsDicts: + localObsDict = dict(self.obsDicts[obs_name]) + obs_filename = build_obs_path( config, component=self.componentName, relativePath=localObsDict['gridFileName']) if f'{var}Var' not in localObsDict.keys(): self.logger.warn( - f'{var}Var is not present in {obsName}, skipping ' - f'{obsName}') + f'{var}Var is not present in {obs_name}, skipping ' + f'{obs_name}') continue - varnameObs = localObsDict[f'{var}Var'] - dsObs = xarray.open_dataset(obsFileName) - dsObs = dsObs.where(obsCellMask, drop=True) - fields.append(dsObs[varnameObs]) - legendText.append(obsName) + obs_var_name = localObsDict[f'{var}Var'] + ds_obs = xarray.open_dataset(obs_filename) + ds_obs = ds_obs.where(obs_cell_mask, drop=True) + fields.append(ds_obs[obs_var_name]) + legendText.append(obs_name) if lineColors is not None: lineColors.append(obsColor) if lineWidths is not None: lineWidths.append([lineWidths[0]]) weights.append(None) if self.controlConfig is not None: - fields.append(dsControl[varname].where(controlCellMask, - drop=True)) - controlRunName = self.controlConfig.get('runs', 'mainRunName') - legendText.append('Control') - title = f'{title} vs. {controlRunName}' + fields.append(ds_control[var_name].where(control_cell_mask, + drop=True)) + control_run_name = self.controlConfig.get('runs', 'mainRunName') + legendText.append(control_run_name) + title = f'Main vs. Control' if lineColors is not None: lineColors.append(obsColor) if lineWidths is not None: lineWidths.append([lineWidths[0]]) - weights.append(dsControlWeights[f'{varname}_weight'].values) + weights.append(ds_control_weights[f'{var_name}_weight'].values) histogram_analysis_plot(config, fields, calendar=calendar, - title=title, xlabel=xLabel, ylabel=yLabel, + title=title, xLabel=xLabel, yLabel=yLabel, bins=bins, weights=weights, lineColors=lineColors, lineWidths=lineWidths, @@ -584,9 +570,9 @@ def run_task(self): titleFontSize=titleFontSize, defaultFontSize=defaultFontSize) - outFileName = f'{self.plotsDirectory}/{self.filePrefix}_{var}_' \ + out_filename = f'{self.plotsDirectory}/{self.filePrefix}_{var}_' \ f'{self.regionName}_{self.season}.png' - savefig(outFileName, config) + savefig(out_filename, config) write_image_xml( config=config, diff --git a/mpas_analysis/shared/plot/histogram.py b/mpas_analysis/shared/plot/histogram.py index 8ec57c1ce..589292c4b 100644 --- a/mpas_analysis/shared/plot/histogram.py +++ b/mpas_analysis/shared/plot/histogram.py @@ -29,7 +29,7 @@ from mpas_analysis.shared.plot.title import limit_title -def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, +def histogram_analysis_plot(config, dsValues, calendar, title, xLabel, yLabel, bins=20, range=None, density=True, weights=None, lineColors=None, lineStyles=None, markers=None, lineWidths=None, legendText=None, @@ -46,14 +46,14 @@ def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, the configuration, containing a [plot] section with options that control plotting - dsvalues : list of xarray DataSets + dsValues : list of xarray DataSets the data set(s) to be plotted. Datasets should already be sliced within the time range specified in the config file. title : str the title of the plot - xlabel, ylabel : str + xLabel, yLabel : str axis labels calendar : str @@ -62,8 +62,8 @@ def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, density : logical if True, normalize the histogram so that the area under the curve is 1 - weights: list of numpy data arrays or NoneType's of length dsvalues - the weights corresponding to each entry in dsvalues + weights: list of numpy data arrays or NoneType's of length dsValues + the weights corresponding to each entry in dsValues lineColors, lineStyles, legendText : list of str, optional control line color, style, and corresponding legend @@ -120,49 +120,49 @@ def histogram_analysis_plot(config, dsvalues, calendar, title, xlabel, ylabel, axis_font = {'size': axisFontSize} ax = plt.gca() - labelCount = 0 - for dsIndex, dsvalue in enumerate(dsvalues): - if dsvalue is None: + label_count = 0 + for ds_index, ds_value in enumerate(dsValues): + if ds_value is None: continue if legendText is None: label = None else: - label = legendText[dsIndex] + label = legendText[ds_index] if label is not None: label = limit_title(label, maxTitleLength) - labelCount += 1 + label_count += 1 if lineColors is None: color = 'k' else: - color = lineColors[dsIndex] + color = lineColors[ds_index] if lineStyles is None: - linestyle = '-' + line_style = '-' else: - linestyle = lineStyles[dsIndex] + line_style = lineStyles[ds_index] if markers is None: marker = None else: marker = markers[dsIndex] if lineWidths is None: - linewidth = 1. + line_width = 1. else: - linewidth = lineWidths[dsIndex] + line_width = lineWidths[ds_index] - hist_values = dsvalue.values.ravel() - weight = weights[dsIndex] + hist_values = ds_value.values.ravel() + weight = weights[ds_index] hist_type = 'step' ax.hist(hist_values, range=range, bins=bins, weights=weight, - linestyle=linestyle, linewidth=linewidth, + linestyle=line_style, linewidth=line_width, histtype=hist_type, label=label, density=density) - if labelCount > 1: + if label_count > 1: plt.legend(loc=legendLocation) if title is not None: title = limit_title(title, maxTitleLength) plt.title(title, **title_font) - if xlabel is not None: - plt.xlabel(xlabel, **axis_font) - if ylabel is not None: - plt.ylabel(ylabel, **axis_font) + if xLabel is not None: + plt.xlabel(xLabel, **axis_font) + if yLabel is not None: + plt.ylabel(yLabel, **axis_font) return fig From cc50fe4ee97d334f22bdb327f28132144f8ac6a4 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Sun, 2 Oct 2022 18:39:25 -0500 Subject: [PATCH 15/26] Add histograms to default.cfg, off by default --- mpas_analysis/default.cfg | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/mpas_analysis/default.cfg b/mpas_analysis/default.cfg index 829276e5f..687f7f34e 100755 --- a/mpas_analysis/default.cfg +++ b/mpas_analysis/default.cfg @@ -3416,6 +3416,33 @@ observationsLabel = SeaWIFS galleryLabel = Chlorophyll +[oceanHistogram] +## options related to plotting histograms of climatologies of 2-d ocean +## variables + +# list of variables to plot +variableList = [] + +# list of observations to compare against +obsList = ['AVISO'] + +# list of ocean variables by which to weight variables in variable list +weightList = [] + +# list of regions to plot from the region list in [regions] below +regionGroups = ['Ocean Basins'] + +# list of region names within the region group listed above +regionNames = [] + +# Seasons to conduct analysis over +# Note: start and end year will be inherited from climatology section +seasons = ['ANN'] + +# Number of histogram bins +bins = 40 + + [oceanRegionalProfiles] ## options related to plotting vertical profiles of regional means (and ## variability) of 3D MPAS fields From 0e41dd6080bd38c9373b83f463a5a802a73ae95c Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Mon, 10 Oct 2022 07:38:11 -0600 Subject: [PATCH 16/26] Apply suggestions from code review Co-authored-by: Xylar Asay-Davis --- mpas_analysis/ocean/histogram.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 64955e8a3..a1a00dc2f 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -138,6 +138,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, self.variableList, self.weightList, season) plotRegionSubtask.run_after(mpasMasksSubtask) plotRegionSubtask.run_after(obsMasksSubtask) + plotRegionSubtask.run_after(computeWeightsSubtask) self.add_subtask(plotRegionSubtask) def setup_and_check(self): @@ -217,7 +218,7 @@ def __init__(self, parentTask, regionName, mpasMasksSubtask, fullSuffix, taskName=parentTask.taskName, componentName=parentTask.componentName, tags=parentTask.tags, - subtaskName=f'weights{fullSuffix}_{regionName}') + subtaskName=f'weights_{regionGroupSuffix}_{regionName}') self.mpasMasksSubtask = mpasMasksSubtask self.regionName = regionName @@ -356,7 +357,7 @@ def __init__(self, parentTask, regionGroup, regionName, controlConfig, taskName=parentTask.taskName, componentName=parentTask.componentName, tags=parentTask.tags, - subtaskName=f'plot{fullSuffix}_{regionName}_{season}') + subtaskName=f'plot_{regionGroupSuffix}_{regionName}_{season}') self.run_after(mpasClimatologyTask) self.regionGroup = regionGroup From 7a9c6413e811c13f832e4e79289a3f04d3314434 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Mon, 10 Oct 2022 08:42:02 -0500 Subject: [PATCH 17/26] Include regionGroup in file and task names --- mpas_analysis/ocean/histogram.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index a1a00dc2f..d17e0ced4 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -77,8 +77,6 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, else: self.weightList = None - self.filePrefix = f'histogram_{mainRunName}' - baseDirectory = build_config_full_path( config, 'output', 'histogramSubdirectory') if not os.path.exists(baseDirectory): @@ -102,6 +100,9 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, regionNames = mpasMasksSubtask.expand_region_names( self.regionNames) + regionGroupSuffix = regionGroup.replace(' ', '_') + filePrefix = f'histogram_{regionGroupSuffix}' + # Add mask subtasks for observations and prep groupObsDicts # groupObsDicts is a subsetted version of localObsDicts with an # additional attribute for the maskTask @@ -124,7 +125,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, # Compute weights for histogram if self.weightList is not None: computeWeightsSubtask = ComputeHistogramWeightsSubtask( - self, regionName, mpasMasksSubtask, self.filePrefix, + self, regionName, mpasMasksSubtask, filePrefix, self.variableList, self.weightList) self.add_subtask(computeWeightsSubtask) @@ -133,7 +134,7 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, # Generate histogram plots plotRegionSubtask = PlotRegionHistogramSubtask( self, regionGroup, regionName, controlConfig, - sectionName, self.filePrefix, mpasClimatologyTask, + sectionName, filePrefix, mpasClimatologyTask, mpasMasksSubtask, obsMasksSubtask, groupObsDicts, self.variableList, self.weightList, season) plotRegionSubtask.run_after(mpasMasksSubtask) @@ -218,7 +219,7 @@ def __init__(self, parentTask, regionName, mpasMasksSubtask, fullSuffix, taskName=parentTask.taskName, componentName=parentTask.componentName, tags=parentTask.tags, - subtaskName=f'weights_{regionGroupSuffix}_{regionName}') + subtaskName=f'weights_{fullSuffix}_{regionName}') self.mpasMasksSubtask = mpasMasksSubtask self.regionName = regionName @@ -357,7 +358,7 @@ def __init__(self, parentTask, regionGroup, regionName, controlConfig, taskName=parentTask.taskName, componentName=parentTask.componentName, tags=parentTask.tags, - subtaskName=f'plot_{regionGroupSuffix}_{regionName}_{season}') + subtaskName=f'plot_{fullSuffix}_{regionName}_{season}') self.run_after(mpasClimatologyTask) self.regionGroup = regionGroup From ec2b8488ca84fdbec6fa7ce9e2c2f1326e10ef2b Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Mon, 10 Oct 2022 08:43:32 -0500 Subject: [PATCH 18/26] Fix typo in ocean histogram task --- mpas_analysis/__main__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mpas_analysis/__main__.py b/mpas_analysis/__main__.py index 828f50374..d4b8eca3b 100755 --- a/mpas_analysis/__main__.py +++ b/mpas_analysis/__main__.py @@ -197,8 +197,9 @@ def build_analysis_list(config, controlConfig): controlConfig)) analyses.append(ocean.TimeSeriesTransport(config, controlConfig)) - analyses.append(ocean.OceanHistogram(config, oceanClimatolgyTasks['avg'], oceanRegionMasksTask, - controlConfig)) + analyses.append(ocean.OceanHistogram(config, oceanClimatologyTasks['avg'], + oceanRegionMasksTask, + controlConfig)) analyses.append(ocean.MeridionalHeatTransport( config, oceanClimatologyTasks['avg'], controlConfig)) From dd29f0cf995f4fb76f7275fc293463ceaae12e64 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Mon, 10 Oct 2022 08:50:45 -0700 Subject: [PATCH 19/26] Fixup mpas_analysis/__main__.py --- mpas_analysis/__main__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mpas_analysis/__main__.py b/mpas_analysis/__main__.py index d4b8eca3b..b61acc94e 100755 --- a/mpas_analysis/__main__.py +++ b/mpas_analysis/__main__.py @@ -214,7 +214,8 @@ def build_analysis_list(config, controlConfig): analyses.append(ocean.SoseTransects(config, oceanClimatologyTasks['avg'], controlConfig)) - analyses.append(ocean.GeojsonTransects(config, oceanClimatologyTasks['avg'], + analyses.append(ocean.GeojsonTransects(config, + oceanClimatologyTasks['avg'], controlConfig)) oceanRegionalProfiles = ocean.OceanRegionalProfiles( @@ -245,16 +246,16 @@ def build_analysis_list(config, controlConfig): hemisphere='SH', controlConfig=controlConfig)) analyses.append(seaIceTimeSeriesTask) analyses.append(sea_ice.ClimatologyMapSeaIceProduction( - config=config, mpas_climatology_task=seaIceClimatolgyTask, + config=config, mpas_climatology_task=seaIceClimatologyTask, hemisphere='NH', control_config=controlConfig)) analyses.append(sea_ice.ClimatologyMapSeaIceProduction( - config=config, mpas_climatology_task=seaIceClimatolgyTask, + config=config, mpas_climatology_task=seaIceClimatologyTask, hemisphere='SH', control_config=controlConfig)) analyses.append(sea_ice.ClimatologyMapSeaIceMelting( - config=config, mpas_climatology_task=seaIceClimatolgyTask, + config=config, mpas_climatology_task=seaIceClimatologyTask, hemisphere='NH', control_config=controlConfig)) analyses.append(sea_ice.ClimatologyMapSeaIceMelting( - config=config, mpas_climatology_task=seaIceClimatolgyTask, + config=config, mpas_climatology_task=seaIceClimatologyTask, hemisphere='SH', control_config=controlConfig)) analyses.append(sea_ice.TimeSeriesSeaIce(config, seaIceTimeSeriesTask, From ba6dacdd6093c923dcbfb2b3d3eced8594a274ea Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Mon, 10 Oct 2022 09:43:41 -0700 Subject: [PATCH 20/26] Fix style --- mpas_analysis/ocean/histogram.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index d17e0ced4..a6eb48a44 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -265,8 +265,8 @@ def run_task(self): ds_weights[f'{var_name}_weight'] = \ ds_restart[weight_var_name].where(cell_mask, drop=True) else: - self.logger.warn(f'Weight variable {weight_var_name} is not in ' - f'the restart file, skipping') + self.logger.warn(f'Weight variable {weight_var_name} is not ' + f'in the restart file, skipping') weights_filename = \ f'{base_directory}/{self.filePrefix}_{self.regionName}_weights.nc' @@ -460,7 +460,8 @@ def run_task(self): control_weights_filename = \ f'{base_directory}/histogram_{control_run_name}_' \ f'{self.regionName}_weights.nc' - ds_control_weights = xarray.open_dataset(control_weights_filename) + ds_control_weights = xarray.open_dataset( + control_weights_filename) if config.has_option(self.taskName, 'lineColors'): lineColors = [config.get(self.taskName, 'mainColor')] @@ -555,7 +556,8 @@ def run_task(self): if self.controlConfig is not None: fields.append(ds_control[var_name].where(control_cell_mask, drop=True)) - control_run_name = self.controlConfig.get('runs', 'mainRunName') + control_run_name = self.controlConfig.get('runs', + 'mainRunName') legendText.append(control_run_name) title = f'Main vs. Control' if lineColors is not None: @@ -573,7 +575,7 @@ def run_task(self): defaultFontSize=defaultFontSize) out_filename = f'{self.plotsDirectory}/{self.filePrefix}_{var}_' \ - f'{self.regionName}_{self.season}.png' + f'{self.regionName}_{self.season}.png' savefig(out_filename, config) write_image_xml( From 2e6c67eb72a935e6cf404527542bee03ce13a215 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Mon, 10 Oct 2022 11:45:45 -0700 Subject: [PATCH 21/26] Change title, caption --- mpas_analysis/ocean/histogram.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index a6eb48a44..af3b3cafb 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -480,7 +480,6 @@ def run_task(self): else: lineWidths = None - title = main_run_name if config.has_option(self.taskName, 'titleFontSize'): titleFontSize = config.getint(self.taskName, 'titleFontSize') @@ -512,8 +511,11 @@ def run_task(self): var_name = f'timeMonthly_avg_{var}' - caption = f'Normalized probability density function for {var} ' \ - f'climatologies in {self.regionName.replace("_", " ")}' + title = f'{self.regionName.replace("_", " ")}, {self.season}' + + caption = f'Normalized probability density function for ' \ + f'{self.season} {var} climatologies in ' \ + f'{self.regionName.replace("_", " ")}' # Note: consider modifying this for more professional headings varTitle = var @@ -559,7 +561,6 @@ def run_task(self): control_run_name = self.controlConfig.get('runs', 'mainRunName') legendText.append(control_run_name) - title = f'Main vs. Control' if lineColors is not None: lineColors.append(obsColor) if lineWidths is not None: From 24cebbd6a221a395dd2869ea03361b79344dbd63 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Mon, 10 Oct 2022 16:01:08 -0700 Subject: [PATCH 22/26] Fixup filePrefix for control run --- mpas_analysis/ocean/histogram.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index af3b3cafb..b9872f3e8 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -451,15 +451,14 @@ def run_task(self): ds_control = xarray.open_dataset(control_filename) base_directory = build_config_full_path( self.controlConfig, 'output', 'histogramSubdirectory') - control_region_mask_filename = f'{base_directory}/histogram_' \ - f'{control_run_name}_{self.regionName}_mask.nc' + control_region_mask_filename = \ + f'{base_directory}/{self.filePrefix}_{self.regionName}_mask.nc' ds_control_region_masks = xarray.open_dataset( control_region_mask_filename) control_cell_mask = ds_control_region_masks.regionCellMasks == 1 if self.weightList is not None: - control_weights_filename = \ - f'{base_directory}/histogram_{control_run_name}_' \ - f'{self.regionName}_weights.nc' + control_weights_filename = f'{base_directory}/' \ + f'{self.filePrefix}_{self.regionName}_weights.nc' ds_control_weights = xarray.open_dataset( control_weights_filename) From 683de8b2af6e48d18606aeeb93af365827f9b42d Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 11 Oct 2022 09:49:36 -0500 Subject: [PATCH 23/26] Fixup masking for histogram vars --- mpas_analysis/ocean/histogram.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index b9872f3e8..6ae883beb 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -433,7 +433,6 @@ def run_task(self): in_filename = get_unmasked_mpas_climatology_file_name( config, self.season, self.componentName, op='avg') ds = xarray.open_dataset(in_filename) - ds = ds.where(cell_mask, drop=True) base_directory = build_config_full_path( config, 'output', 'histogramSubdirectory') @@ -519,7 +518,7 @@ def run_task(self): # Note: consider modifying this for more professional headings varTitle = var - fields.append(ds[var_name]) + fields.append(ds[var_name].where(cell_mask, drop=True)) if self.weightList is not None: if f'{var_name}_weight' in ds_weights.keys(): weights.append(ds_weights[f'{var_name}_weight'].values) From abc68957128e60274de84b8608ea895b4e731816 Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 11 Oct 2022 10:23:18 -0500 Subject: [PATCH 24/26] Fixup weightList checks --- mpas_analysis/ocean/histogram.py | 36 ++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 6ae883beb..69287f153 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -74,6 +74,11 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, self.variableList = config.getexpression(self.taskName, 'variableList') if config.has_option(self.taskName, 'weightList'): self.weightList = config.getexpression(self.taskName, 'weightList') + if not self.weightList: + self.weightList = None + elif len(self.weightList) != len(self.variableList): + raise ValueError('Histogram weightList is not the same ' + 'length as variableList') else: self.weightList = None @@ -139,7 +144,8 @@ def __init__(self, config, mpasClimatologyTask, regionMasksTask, self.variableList, self.weightList, season) plotRegionSubtask.run_after(mpasMasksSubtask) plotRegionSubtask.run_after(obsMasksSubtask) - plotRegionSubtask.run_after(computeWeightsSubtask) + if self.weightList is not None: + plotRegionSubtask.run_after(computeWeightsSubtask) self.add_subtask(plotRegionSubtask) def setup_and_check(self): @@ -165,10 +171,6 @@ def setup_and_check(self): self.mpasClimatologyTask.add_variables(variableList=variableList, seasons=self.seasons) - if self.weightList is not None: - if len(self.weightList) != len(self.variableList): - raise ValueError('Histogram weightList is not the same ' - 'length as variableList') if len(self.obsList) > 1: raise ValueError('Histogram analysis does not currently support' 'more than one observational product') @@ -256,17 +258,19 @@ def run_task(self): f'{base_directory}/{self.filePrefix}_{self.regionName}_mask.nc' write_netcdf(ds_mask, new_region_mask_filename) - ds_weights = xarray.Dataset() - # 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(): - var_name = f'timeMonthly_avg_{var}' - ds_weights[f'{var_name}_weight'] = \ - ds_restart[weight_var_name].where(cell_mask, drop=True) - else: - self.logger.warn(f'Weight variable {weight_var_name} is not ' - f'in the restart file, skipping') + if self.weightList is not None: + print(self.weightList) + ds_weights = xarray.Dataset() + # 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(): + var_name = f'timeMonthly_avg_{var}' + ds_weights[f'{var_name}_weight'] = \ + ds_restart[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') weights_filename = \ f'{base_directory}/{self.filePrefix}_{self.regionName}_weights.nc' From c836ee278088e0053c2a2fb55afed62d11087d4f Mon Sep 17 00:00:00 2001 From: Carolyn Begeman Date: Tue, 11 Oct 2022 10:55:15 -0500 Subject: [PATCH 25/26] Add oceanHistogram to user's guide --- docs/users_guide/analysis_tasks.rst | 1 + docs/users_guide/config/regions.rst | 12 +- .../examples/histogram_ssh_aviso_atl.png | Bin 0 -> 101739 bytes docs/users_guide/tasks/oceanHistogram.rst | 120 ++++++++++++++++++ 4 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 docs/users_guide/tasks/examples/histogram_ssh_aviso_atl.png create mode 100644 docs/users_guide/tasks/oceanHistogram.rst diff --git a/docs/users_guide/analysis_tasks.rst b/docs/users_guide/analysis_tasks.rst index ee7e23f2f..7c34277d0 100644 --- a/docs/users_guide/analysis_tasks.rst +++ b/docs/users_guide/analysis_tasks.rst @@ -35,6 +35,7 @@ Analysis Tasks tasks/geojsonTransects tasks/oceanRegionalProfiles tasks/regionalTSDiagrams + tasks/oceanHistogram tasks/climatologyMapSeaIceConcNH tasks/climatologyMapSeaIceThickNH diff --git a/docs/users_guide/config/regions.rst b/docs/users_guide/config/regions.rst index d0de623aa..fb83a7c7a 100644 --- a/docs/users_guide/config/regions.rst +++ b/docs/users_guide/config/regions.rst @@ -35,17 +35,17 @@ web page. Region Groups ------------- -Currently, seven analysis tasks (:ref:`task_climatologyMapAntarcticMelt`, +Currently, eight analysis tasks (:ref:`task_climatologyMapAntarcticMelt`, :ref:`task_hovmollerOceanRegions`, :ref:`task_oceanRegionalProfiles`, :ref:`task_regionalTSDiagrams`, :ref:`task_streamfunctionMOC`, -:ref:`task_timeSeriesAntarcticMelt`, and :ref:`task_timeSeriesOceanRegions`) -use masks that define regions in an MPAS mesh as part of their analysis. Most -of these region group are defined in +:ref:`task_oceanHistogram`, :ref:`task_timeSeriesAntarcticMelt`, and +:ref:`task_timeSeriesOceanRegions`) use masks that define regions in an MPAS +mesh as part of their analysis. Most of these region group are defined in :py:func:`geometric_features.aggregation.get_aggregator_by_name()`. -Several tasks (:ref:`task_hovmollerOceanRegions`, +Several tasks (:ref:`task_hovmollerOceanRegions`, :ref:`task_oceanHistogram`, :ref:`task_oceanRegionalProfiles`, :ref:`task_regionalTSDiagrams`, and :ref:`task_timeSeriesOceanRegions`) can use any of the defined region groups. -Currently, available region groups are: ``Antarctic Regions``, +Currently, available region groups are: ``Artic Ocean Regions``, ``Antarctic Regions``, ``Ocean Basins``, ``Ice Shelves``, and ``Ocean Subbasins``. The option ``regionMaskSubdirectory`` in the ``[diagnostics]`` section specifies diff --git a/docs/users_guide/tasks/examples/histogram_ssh_aviso_atl.png b/docs/users_guide/tasks/examples/histogram_ssh_aviso_atl.png new file mode 100644 index 0000000000000000000000000000000000000000..166351a4b8e4cc21d79690cda9ca65bf67ae8277 GIT binary patch literal 101739 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 Date: Tue, 11 Oct 2022 17:11:00 -0500 Subject: [PATCH 26/26] Fixup config options --- mpas_analysis/ocean/histogram.py | 44 ++++++++++++++------------ mpas_analysis/shared/plot/histogram.py | 2 +- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/mpas_analysis/ocean/histogram.py b/mpas_analysis/ocean/histogram.py index 69287f153..eb44753ff 100644 --- a/mpas_analysis/ocean/histogram.py +++ b/mpas_analysis/ocean/histogram.py @@ -259,7 +259,6 @@ def run_task(self): write_netcdf(ds_mask, new_region_mask_filename) if self.weightList is not None: - print(self.weightList) ds_weights = xarray.Dataset() # Fetch the weight variables and mask them for each region for index, var in enumerate(self.variableList): @@ -465,29 +464,30 @@ def run_task(self): ds_control_weights = xarray.open_dataset( control_weights_filename) - if config.has_option(self.taskName, 'lineColors'): - lineColors = [config.get(self.taskName, 'mainColor')] + if config.has_option(self.taskName, 'mainColor'): + mainColor = config.get(self.taskName, 'mainColor') else: - lineColors = None + mainColor = 'C0' if config.has_option(self.taskName, 'obsColor'): - obsColor = config.get_expression(self.taskName, 'obsColor') - if lineColors is None: - lineColors = ['b'] + obsColor = config.get(self.taskName, 'obsColor') else: - if lineColors is not None: - obsColor = 'k' + obsColor = 'C1' + if config.has_option(self.taskName, 'controlColor'): + controlColor = config.get(self.taskName, 'controlColor') + else: + controlColor = 'C2' - if config.has_option(self.taskName, 'lineWidths'): - lineWidths = [config.get(self.taskName, 'lineWidths')] + if config.has_option(self.taskName, 'lineWidth'): + lineWidth = config.getfloat(self.taskName, 'lineWidth') else: - lineWidths = None + lineWidth = None if config.has_option(self.taskName, 'titleFontSize'): titleFontSize = config.getint(self.taskName, 'titleFontSize') else: titleFontSize = None - if config.has_option(self.taskName, 'titleFontSize'): + if config.has_option(self.taskName, 'axisFontSize'): axisFontSize = config.getint(self.taskName, 'axisFontSize') else: @@ -510,6 +510,7 @@ def run_task(self): fields = [] weights = [] legendText = [] + lineColors = [] var_name = f'timeMonthly_avg_{var}' @@ -533,6 +534,7 @@ def run_task(self): weights.append(None) legendText.append(main_run_name) + lineColors.append(mainColor) xLabel = f"{ds[var_name].attrs['long_name']} " \ f"({ds[var_name].attrs['units']})" @@ -552,10 +554,7 @@ def run_task(self): ds_obs = ds_obs.where(obs_cell_mask, drop=True) fields.append(ds_obs[obs_var_name]) legendText.append(obs_name) - if lineColors is not None: - lineColors.append(obsColor) - if lineWidths is not None: - lineWidths.append([lineWidths[0]]) + lineColors.append(obsColor) weights.append(None) if self.controlConfig is not None: fields.append(ds_control[var_name].where(control_cell_mask, @@ -563,11 +562,14 @@ def run_task(self): control_run_name = self.controlConfig.get('runs', 'mainRunName') legendText.append(control_run_name) - if lineColors is not None: - lineColors.append(obsColor) - if lineWidths is not None: - lineWidths.append([lineWidths[0]]) + lineColors.append(controlColor) weights.append(ds_control_weights[f'{var_name}_weight'].values) + + if lineWidth is not None: + lineWidths = [lineWidth for i in fields] + else: + lineWidths = None + histogram_analysis_plot(config, fields, calendar=calendar, title=title, xLabel=xLabel, yLabel=yLabel, bins=bins, weights=weights, diff --git a/mpas_analysis/shared/plot/histogram.py b/mpas_analysis/shared/plot/histogram.py index 589292c4b..29e8f42bb 100644 --- a/mpas_analysis/shared/plot/histogram.py +++ b/mpas_analysis/shared/plot/histogram.py @@ -152,7 +152,7 @@ def histogram_analysis_plot(config, dsValues, calendar, title, xLabel, yLabel, weight = weights[ds_index] hist_type = 'step' ax.hist(hist_values, range=range, bins=bins, weights=weight, - linestyle=line_style, linewidth=line_width, + color=color, linestyle=line_style, linewidth=line_width, histtype=hist_type, label=label, density=density) if label_count > 1: plt.legend(loc=legendLocation)