# -*- 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/main/LICENSE
#
import os
import xarray
from mpas_analysis.shared import AnalysisTask
from mpas_analysis.shared.plot import timeseries_analysis_plot, savefig
from mpas_analysis.shared.io import open_mpas_dataset, write_netcdf_with_fill
from mpas_analysis.shared.timekeeping.utility import date_to_days, \
days_to_datetime
from mpas_analysis.shared.io.utility import build_config_full_path, \
make_directories
from mpas_analysis.shared.html import write_image_xml
from mpas_analysis.shared.time_series import compute_moving_avg, \
combine_time_series_with_ncrcat
[docs]
class PlotDepthIntegratedTimeSeriesSubtask(AnalysisTask):
"""
Plots a time series, summed or averaged over various depth ranges
Attributes
----------
regionName : str
The name of the region to plot
inFileName : str
The file containing the time-depth data set to plot
outFileLabel : str
The prefix on each plot and associated XML file
fieldNameInTitle : str
The name of the field being plotted, as used in the plot title
mpasFieldName : str
The name of the variable in the MPAS timeSeriesStatsMonthly output
yAxisLabel : str
the y-axis label of the plotted field (including units)
sectionName : str
A section in the config file where the colormap and contour values
are defined
thumbnailSuffix : str
The text to be displayed under the thumbnail image, to which the
region name will be prepended
imageCaption : str
The caption when mousing over the plot or displaying it full
screen
galleryGroup : str
The name of the group of galleries in which this plot belongs
groupSubtitle : str
The subtitle of the group in which this plot belongs (or blank
if none)
groupLink : str
A short name (with no spaces) for the link to the gallery group
galleryName : str
The name of the gallery in which this plot belongs
controlConfig : mpas_tools.config.MpasConfigParser
The configuration options for the control run (if any)
"""
# Authors
# -------
# Xylar Asay-Davis, Milena Veneziani, Greg Streletz
[docs]
def __init__(self, parentTask, regionName, inFileName, outFileLabel,
fieldNameInTitle, mpasFieldName, yAxisLabel, sectionName,
thumbnailSuffix, imageCaption, galleryGroup, groupSubtitle,
groupLink, galleryName, subtaskName=None, controlConfig=None):
"""
Construct the analysis task.
Parameters
----------
parentTask : ``AnalysisTask``
The parent task of which this is a subtask
regionName : str
The name of the region to plot
inFileName : str
The file containing the time-depth data set to plot
outFileLabel : str
The prefix on each plot and associated XML file
fieldNameInTitle : str
The name of the field being plotted, as used in the plot title
mpasFieldName : str
The name of the variable in the MPAS timeSeriesStatsMonthly output
yAxisLabel : str
the y-axis label of the plotted field
sectionName : str
a section in the config file where the colormap and contour values
are defined
thumbnailSuffix : str
The text to be displayed under the thumbnail image, to which the
region name will be prepended
imageCaption : str
the caption when mousing over the plot or displaying it full
screen
galleryGroup : str
the name of the group of galleries in which this plot belongs
groupSubtitle : str
the subtitle of the group in which this plot belongs (or blank
if none)
groupLink : str
a short name (with no spaces) for the link to the gallery group
galleryName : str
the name of the gallery in which this plot belongs
subtaskName : str, optional
The name of the subtask (``plotTimeSeries<RegionName>`` by default)
controlConfig : mpas_tools.config.MpasConfigParser, optional
The configuration options for the control run (if any)
"""
# Authors
# -------
# Xylar Asay-Davis
if subtaskName is None:
suffix = regionName[0].upper() + regionName[1:]
subtaskName = 'plotDepthIntegratedTimeSeries{}'.format(suffix)
# first, call the constructor from the base class (AnalysisTask)
super(PlotDepthIntegratedTimeSeriesSubtask, self).__init__(
config=parentTask.config,
taskName=parentTask.taskName,
componentName='ocean',
tags=parentTask.tags,
subtaskName=subtaskName)
self.regionName = regionName
self.inFileName = inFileName
self.outFileLabel = outFileLabel
self.fieldNameInTitle = fieldNameInTitle
self.mpasFieldName = mpasFieldName
self.yAxisLabel = yAxisLabel
self.sectionName = sectionName
self.controlConfig = controlConfig
# xml/html related variables
self.thumbnailSuffix = thumbnailSuffix
self.imageCaption = imageCaption
self.galleryGroup = galleryGroup
self.groupSubtitle = groupSubtitle
self.groupLink = groupLink
self.galleryName = galleryName
def setup_and_check(self):
"""
Perform steps to set up the analysis and check for errors in the setup.
"""
# Authors
# -------
# Xylar Asay-Davis, Greg Streletz
# first, call setup_and_check from the base class (AnalysisTask),
# which will perform some common setup, including storing:
# self.runDirectory , self.historyDirectory, self.plotsDirectory,
# self.namelist, self.runStreams, self.historyStreams,
# self.calendar
super(PlotDepthIntegratedTimeSeriesSubtask, self).setup_and_check()
config = self.config
if self.controlConfig is not None:
# we need to know what file to read from the control run so
# an absolute path won't work
assert(not os.path.isabs(self.inFileName))
baseDirectory = build_config_full_path(
self.controlConfig, 'output', 'timeSeriesSubdirectory')
self.refFileName = '{}/{}'.format(baseDirectory,
self.inFileName)
preprocessedReferenceRunName = config.get(
'runs', 'preprocessedReferenceRunName')
if preprocessedReferenceRunName != 'None':
assert(not os.path.isabs(self.inFileName))
baseDirectory = build_config_full_path(
config, 'output', 'timeSeriesSubdirectory')
make_directories('{}/preprocessed'.format(baseDirectory))
self.preprocessedIntermediateFileName = \
'{}/preprocessed/intermediate_{}'.format(baseDirectory,
self.inFileName)
self.preprocessedFileName = '{}/preprocessed/{}'.format(
baseDirectory, self.inFileName)
if not os.path.isabs(self.inFileName):
baseDirectory = build_config_full_path(
config, 'output', 'timeSeriesSubdirectory')
self.inFileName = '{}/{}'.format(baseDirectory,
self.inFileName)
mainRunName = self.config.get('runs', 'mainRunName')
self.filePrefix = '{}_{}_{}'.format(self.outFileLabel,
self.regionName,
mainRunName)
self.xmlFileNames = ['{}/{}.xml'.format(
self.plotsDirectory, self.filePrefix)]
return
def run_task(self):
"""
Compute vertical aggregates of the data and plot the time series
"""
# Authors
# -------
# Xylar Asay-Davis, Milena Veneziani, Greg Streletz
self.logger.info("\nPlotting depth-integrated time series of "
"{}...".format(self.fieldNameInTitle))
config = self.config
calendar = self.calendar
mainRunName = config.get('runs', 'mainRunName')
plotTitles = config.getexpression('regions', 'plotTitles')
allRegionNames = config.getexpression('regions', 'regions')
regionIndex = allRegionNames.index(self.regionName)
regionNameInTitle = plotTitles[regionIndex]
startDate = config.get('timeSeries', 'startDate')
endDate = config.get('timeSeries', 'endDate')
# Load data
self.logger.info(' Load ocean data...')
ds = open_mpas_dataset(fileName=self.inFileName,
calendar=calendar,
variableList=[self.mpasFieldName, 'depth'],
timeVariableNames=None,
startDate=startDate,
endDate=endDate)
ds = ds.isel(nOceanRegionsTmp=regionIndex)
depths = ds.depth.values
divisionDepths = config.getexpression(self.sectionName, 'depths')
# for each depth interval to plot, determine the top and bottom depth
topDepths = [0, 0] + divisionDepths
bottomDepths = [depths[-1]] + divisionDepths + [depths[-1]]
legends = []
for top, bottom in zip(topDepths, bottomDepths):
if bottom == depths[-1]:
legends.append('{}m-bottom'.format(top))
else:
legends.append('{}m-{}m'.format(top, bottom))
# more possible symbols than we typically use
lines = ['-', '-', '--', None, None, None, None]
markers = [None, None, None, '+', 'o', '^', 'v']
widths = [5, 3, 3, 3, 3, 3, 3]
points = [None, None, None, 300, 300, 300, 300]
color = config.get('timeSeries', 'mainColor')
xLabel = 'Time [years]'
yLabel = self.yAxisLabel
title = '{}, {} \n {} (black)'.format(self.fieldNameInTitle,
regionNameInTitle,
mainRunName)
outFileName = '{}/{}.png'.format(self.plotsDirectory, self.filePrefix)
timeSeries = []
lineColors = []
lineStyles = []
lineMarkers = []
lineWidths = []
maxPoints = []
legendText = []
for rangeIndex in range(len(topDepths)):
top = topDepths[rangeIndex]
bottom = bottomDepths[rangeIndex]
field = ds[self.mpasFieldName].where(ds.depth > top)
field = field.where(ds.depth <= bottom)
timeSeries.append(field.sum('nVertLevels'))
lineColors.append(color)
lineStyles.append(lines[rangeIndex])
lineMarkers.append(markers[rangeIndex])
lineWidths.append(widths[rangeIndex])
maxPoints.append(points[rangeIndex])
legendText.append(legends[rangeIndex])
preprocessedReferenceRunName = config.get(
'runs', 'preprocessedReferenceRunName')
if preprocessedReferenceRunName != 'None':
preprocessedInputDirectory = config.get(
'oceanPreprocessedReference', 'baseDirectory')
self.logger.info(' Load in preprocessed reference data...')
preprocessedFilePrefix = config.get(self.sectionName,
'preprocessedFilePrefix')
inFilesPreprocessed = '{}/{}.{}.year*.nc'.format(
preprocessedInputDirectory, preprocessedFilePrefix,
preprocessedReferenceRunName)
combine_time_series_with_ncrcat(
inFilesPreprocessed, self.preprocessedIntermediateFileName,
logger=self.logger)
dsPreprocessed = open_mpas_dataset(
fileName=self.preprocessedIntermediateFileName,
calendar=calendar,
timeVariableNames='xtime')
yearStart = days_to_datetime(ds.Time.min(), calendar=calendar).year
yearEnd = days_to_datetime(ds.Time.max(), calendar=calendar).year
timeStart = date_to_days(year=yearStart, month=1, day=1,
calendar=calendar)
timeEnd = date_to_days(year=yearEnd, month=12, day=31,
calendar=calendar)
yearEndPreprocessed = days_to_datetime(dsPreprocessed.Time.max(),
calendar=calendar).year
if yearStart <= yearEndPreprocessed:
dsPreprocessed = dsPreprocessed.sel(Time=slice(timeStart,
timeEnd))
else:
self.logger.warning('Warning: Preprocessed time series ends '
'before the timeSeries startYear and will '
'not be plotted.')
preprocessedReferenceRunName = 'None'
# rolling mean seems to have trouble with dask data sets so we
# write out the data set and read it back as a single-file data set
# (without dask)
dsPreprocessed = dsPreprocessed.drop_vars('xtime')
write_netcdf_with_fill(dsPreprocessed, self.preprocessedFileName)
dsPreprocessed = xarray.open_dataset(self.preprocessedFileName)
if preprocessedReferenceRunName != 'None':
color = 'purple'
title = '{} \n {} (purple)'.format(title,
preprocessedReferenceRunName)
preprocessedFieldPrefix = config.get(self.sectionName,
'preprocessedFieldPrefix')
movingAveragePoints = config.getint(self.sectionName,
'movingAveragePoints')
suffixes = ['tot'] + ['{}m'.format(depth) for depth in
divisionDepths] + ['btm']
# these preprocessed data are already anomalies
dsPreprocessed = compute_moving_avg(dsPreprocessed,
movingAveragePoints)
for rangeIndex in range(len(suffixes)):
variableName = '{}_{}'.format(preprocessedFieldPrefix,
suffixes[rangeIndex])
if variableName in list(dsPreprocessed.data_vars.keys()):
timeSeries.append(dsPreprocessed[variableName])
else:
self.logger.warning('Warning: Preprocessed variable {} '
'not found. Skipping.'.format(
variableName))
timeSeries.extend(None)
lineColors.append(color)
lineStyles.append(lines[rangeIndex])
lineMarkers.append(markers[rangeIndex])
lineWidths.append(widths[rangeIndex])
maxPoints.append(points[rangeIndex])
legendText.append(None)
if self.controlConfig is not None:
controlRunName = self.controlConfig.get('runs', 'mainRunName')
title = '{} \n {} (red)'.format(title, controlRunName)
self.logger.info(' Load ocean data from control run...')
controlStartYear = self.controlConfig.getint('timeSeries',
'startYear')
controlEndYear = self.controlConfig.getint('timeSeries',
'endYear')
controlStartDate = '{:04d}-01-01_00:00:00'.format(controlStartYear)
controlEndDate = '{:04d}-12-31_23:59:59'.format(controlEndYear)
dsRef = open_mpas_dataset(fileName=self.refFileName,
calendar=calendar,
variableList=[self.mpasFieldName,
'depth'],
timeVariableNames=None,
startDate=controlStartDate,
endDate=controlEndDate)
dsRef = dsRef.isel(nOceanRegionsTmp=regionIndex)
color = config.get('timeSeries', 'controlColor')
for rangeIndex in range(len(topDepths)):
top = topDepths[rangeIndex]
bottom = bottomDepths[rangeIndex]
field = dsRef[self.mpasFieldName].where(dsRef.depth > top)
field = field.where(dsRef.depth <= bottom)
timeSeries.append(field.sum('nVertLevels'))
lineColors.append(color)
lineStyles.append(lines[rangeIndex])
lineMarkers.append(markers[rangeIndex])
lineWidths.append(widths[rangeIndex])
maxPoints.append(points[rangeIndex])
legendText.append(None)
if config.has_option(self.taskName, 'firstYearXTicks'):
firstYearXTicks = config.getint(self.taskName,
'firstYearXTicks')
else:
firstYearXTicks = None
if config.has_option(self.taskName, 'yearStrideXTicks'):
yearStrideXTicks = config.getint(self.taskName,
'yearStrideXTicks')
else:
yearStrideXTicks = None
fig = timeseries_analysis_plot(
config=config, dsvalues=timeSeries, calendar=calendar,
title=title, xlabel=xLabel, ylabel=yLabel, movingAveragePoints=None,
lineColors=lineColors, lineStyles=lineStyles, markers=lineMarkers,
lineWidths=lineWidths, legendText=legendText, maxPoints=maxPoints,
firstYearXTicks=firstYearXTicks, yearStrideXTicks=yearStrideXTicks)
self.customize_fig(fig)
savefig(outFileName, config)
write_image_xml(
config=config,
filePrefix=self.filePrefix,
componentName='Ocean',
componentSubdirectory='ocean',
galleryGroup=self.galleryGroup,
groupLink=self.groupLink,
gallery=self.galleryName,
thumbnailDescription='{} {}'.format(self.regionName,
self.thumbnailSuffix),
imageDescription=self.imageCaption,
imageCaption=self.imageCaption)
def customize_fig(self, fig):
"""
A function to override to customize the figure.
fig : matplotlib.pyplot.Figure
The figure
"""
pass