Module gdess.data_source.models.cmip.cmip_collection
Expand source code
from typing import Union, Sequence
import os, argparse, logging, warnings
import intake
import numpy as np
import pandas as pd
import xarray as xr
import matplotlib.pyplot as plt
from gdess import set_verbose, load_config_file, benchmark_recipe
from gdess.data_source.models.cmip.cmip_name_utils import model_name_dict_from_valid_form, \
matched_model_and_experiment, cmip_model_choices
from gdess.data_source.multiset import Multiset
from gdess.operations.datasetdict import DatasetDict
from gdess.operations.time import ensure_dataset_datetime64
from gdess.operations.convert import co2_molfrac_to_ppm
from gdess.recipe_parsers import add_shared_arguments_for_recipes, parse_recipe_options
from gdess.formatters.args import nullable_str
from gdess.formatters import append_before_extension
from gdess.graphics.single_source_plots import plot_annual_series, plot_zonal_mean
from gdess.graphics.utils import aesthetic_grid_no_spines, mysavefig
_logger = logging.getLogger("{0}.{1}".format(__name__, "loader"))
default_cmip6_datastore_url = "https://raw.githubusercontent.com/NCAR/intake-esm-datastore/master/catalogs/pangeo-cmip6.json"
class Collection(Multiset):
def __init__(self, datastore='cmip6', verbose: Union[bool, str] = False):
"""Instantiate a CMIP Collection object.
Parameters
----------
datastore : str
a shortened name of an ESM catalog that we will query for model outputs.
verbose : Union[bool, str]
can be either True, False, or a string for level such as "INFO, DEBUG, etc."
"""
# Set up the level of verbosity, i.e. how many log messages are displayed.
set_verbose(_logger, verbose)
self._progressbar = True
if _logger.level > 10: # 10 is debug, 20 is info, etc.
self._progressbar = False
self.latest_searched_model_catalog = None
self.catalog_dataframe = None
# TODO: add capability to handle additional datastores besides cmip6, such as CMIP5, etc.
if datastore == 'cmip6':
self.datastore_url = default_cmip6_datastore_url
else:
raise ValueError('Unexpected/unhandled datastore <%s>', datastore)
super().__init__(verbose=verbose)
@classmethod
def _recipe_base(cls,
datastore='cmip6',
verbose: Union[bool, str] = False,
load_method: str = 'pangeo',
pickle_file: str = None,
skip_selections: bool = False,
selection: dict = None,
mean_dims: tuple = None,
model_name: Union[str, list] = None
) -> ('Collection', bool):
"""Create an instance, and either preprocess or load already processed data.
Parameters
----------
datastore : str, default 'cmip6'
verbose : Union[bool, str], default False
load_method : str, default 'pangeo'
either 'pangeo', 'local', or 'pickle'
pickle_file : str
(Optional) path to a saved pickle file is used if argument load_method=='pickle
selection : dict
mean_dims : tuple
Returns
-------
tuple : (Collection, bool)
Collection
bool
Whether datasets were loaded from a pickle file or not. (If not, there is probably more processing needed.)
"""
# An empty instance is created.
new_self = cls(datastore=datastore, verbose=verbose)
_logger.debug(' skip_selections: %s', skip_selections)
loaded_from_pickle_bool = False
if load_method == 'pickle':
# If a valid filename is provided, datasets are loaded into stepC attribute and this is True,
# otherwise, this is False.
loaded_from_pickle_bool = new_self.datasets_from_pickle(filename=pickle_file, replace=True)
_logger.debug(' loaded from pickle? --> %s', loaded_from_pickle_bool)
else:
# Data are formatted into the basic data structure common to various diagnostics.
new_self._load_data(method=load_method, url=new_self.datastore_url, model_name=model_name)
new_self.preprocess()
if not skip_selections:
_logger.debug(' applying selected bounds: %s', selection)
new_self.stepC_prepped_datasets = new_self.stepB_preprocessed_datasets.queue_selection(**selection,
inplace=False)
# Spatial mean is calculated, leaving us with a time series.
new_self.stepC_prepped_datasets.queue_mean(dim=mean_dims, inplace=True)
# The lazily loaded selections and computations are here actually processed.
new_self.stepC_prepped_datasets.execute_all(inplace=True)
return new_self, loaded_from_pickle_bool
@classmethod
@benchmark_recipe
def run_recipe_for_timeseries(cls,
datastore='cmip6',
verbose: Union[bool, str] = False,
pickle_file: str = None,
options: dict = None
) -> 'Collection':
"""Execute a series of preprocessing steps and generate a diagnostic result.
Parameters
----------
datastore : str, default 'cmip6'
verbose : Union[bool, str]
can be either True, False, or a string for level such as "INFO, DEBUG, etc."
pickle_file : str
path to pickled datastore
options
A dictionary with zero or more of these parameter keys:
start_yr : str, default '1960'
end_yr : str, default None
plev : int, default 100000
Returns
-------
Collection object for CMIP6 that was used to generate the diagnostic
"""
set_verbose(_logger, verbose)
opts = parse_recipe_options(options, add_cmip_collection_args_to_parser)
# Apply diagnostic options and prep data for plotting
selection = {'time': slice(opts.start_datetime, opts.end_datetime),
'plev': opts.plev}
new_self, _ = cls._recipe_base(datastore=datastore, verbose=verbose, pickle_file=pickle_file,
selection=selection, mean_dims=('lon', 'lat'),
model_name=opts.model_name, load_method=opts.cmip_load_method)
# --- Plotting ---
fig, ax, bbox_artists = new_self.plot_timeseries()
if opts.figure_savepath:
mysavefig(fig=fig, plot_save_name=append_before_extension(opts.figure_savepath, 'cmip_timeseries'),
bbox_extra_artists=bbox_artists)
return new_self
@classmethod
@benchmark_recipe
def run_recipe_for_vertical_profile(cls,
datastore='cmip6',
verbose: Union[bool, str] = False,
pickle_file: str = None,
options: dict = None
) -> 'Collection':
"""Execute a series of preprocessing steps and generate a diagnostic result.
Parameters
----------
datastore : str
verbose : Union[bool, str]
can be either True, False, or a string for level such as "INFO, DEBUG, etc."
pickle_file : str
path to pickled datastore
options
A dictionary with zero or more of these parameter keys:
start_yr : str, default '1960'
end_yr : str, default None
Returns
-------
Collection object for CMIP6 that was used to generate the diagnostic
"""
set_verbose(_logger, verbose)
opts = parse_recipe_options(options, add_cmip_collection_args_to_parser)
# Apply diagnostic options and prep data for plotting
selection = {'time': slice(opts.start_datetime, opts.end_datetime)}
new_self, _ = cls._recipe_base(datastore=datastore, verbose=verbose, pickle_file=pickle_file,
selection=selection, mean_dims=('lon', 'lat', 'time'),
model_name=opts.model_name, load_method=opts.cmip_load_method)
# --- Plotting ---
fig, ax, bbox_artists = new_self.plot_vertical_profiles()
if opts.figure_savepath:
mysavefig(fig=fig, plot_save_name=append_before_extension(opts.figure_savepath, 'cmip_vertical_plot'),
bbox_extra_artists=bbox_artists)
return new_self
@classmethod
@benchmark_recipe
def run_recipe_for_zonal_mean(cls,
datastore='cmip6',
verbose: Union[bool, str] = False,
pickle_file: str = None,
options: dict = None
) -> 'Collection':
"""Execute a series of preprocessing steps and generate a diagnostic result.
Parameters
----------
datastore : str
verbose : Union[bool, str]
can be either True, False, or a string for level such as "INFO, DEBUG, etc."
pickle_file : str
path to pickled datastore
options
A dictionary with zero or more of these parameter keys:
start_yr : str, default '1960'
end_yr : str, default None
Returns
-------
Collection object for CMIP6 that was used to generate the diagnostic
"""
set_verbose(_logger, verbose)
opts = parse_recipe_options(options, add_cmip_collection_args_to_parser)
# Apply diagnostic options and prep data for plotting
selection = {'time': slice(opts.start_datetime, opts.end_datetime)}
new_self, _ = cls._recipe_base(datastore=datastore, verbose=verbose, pickle_file=pickle_file,
selection=selection, mean_dims=('lon', 'time'),
model_name=opts.model_name, load_method=opts.cmip_load_method)
if not opts.member_key:
_logger.debug("No 'member_key' supplied. Averaging over the available members: %s",
new_self.stepC_prepped_datasets[opts.model_name]['member_id'].values.tolist())
opts.member_key = new_self.stepC_prepped_datasets[opts.model_name]['member_id'].values.tolist()
# --- Plotting ---
#
darray = new_self.stepC_prepped_datasets[opts.model_name].sel(member_id=opts.member_key)['co2']
fig, ax, bbox_artists = plot_zonal_mean(darray, titlestr=f"{opts.model_name} ({opts.member_key})")
if opts.figure_savepath:
mysavefig(fig=fig, plot_save_name=append_before_extension(opts.figure_savepath, 'cmip_zonal_mean_plot'),
bbox_extra_artists=bbox_artists)
return new_self
@classmethod
@benchmark_recipe
def run_recipe_for_annual_series(cls,
datastore='cmip6',
verbose: Union[bool, str] = False,
pickle_file: str = None,
options: dict = None
) -> 'Collection':
"""Execute a series of preprocessing steps and generate a diagnostic result.
Parameters
----------
datastore : str
verbose : Union[bool, str]
can be either True, False, or a string for level such as "INFO, DEBUG, etc."
pickle_file : str
path to pickled datastore
options
A dictionary with zero or more of these parameter keys:
start_yr : str, default '1960'
end_yr : str, default None
Returns
-------
Collection object for CMIP6 that was used to generate the diagnostic
"""
set_verbose(_logger, verbose)
opts = parse_recipe_options(options, add_cmip_collection_args_to_parser)
# Apply diagnostic options and prep data for plotting
selection = {'time': slice(opts.start_datetime, opts.end_datetime),
'plev': opts.plev}
new_self, _ = cls._recipe_base(datastore=datastore, verbose=verbose, pickle_file=pickle_file,
selection=selection, mean_dims=('lon', 'lat'),
model_name=opts.model_name, load_method=opts.cmip_load_method)
ds = new_self.stepC_prepped_datasets[opts.model_name]
if not ('member_id' in ds.dims.keys()):
df_anomaly_mean_cycle, df_anomaly_yearly = Multiset.get_anomaly_dataframes(ds, varname='co2')
else:
if not opts.member_key:
_logger.debug("No 'member_key' supplied. Using the available member(s): %s",
ds['member_id'].values.tolist())
opts.member_key = ds['member_id'].values.tolist()
if isinstance(opts.member_key, str) or (len(opts.member_key) == 1):
ds = new_self.stepC_prepped_datasets[opts.model_name].sel(member_id=opts.member_key)
df_anomaly_mean_cycle, df_anomaly_yearly = Multiset.get_anomaly_dataframes(ds, varname='co2')
elif isinstance(opts.member_key, list) and (len(opts.member_key) > 1):
# The mean is calculated across ensemble members if there are multiple.
df_list_of_means = []
df_list_of_yearly_cycles = []
for mi, m in enumerate(opts.member_key):
_logger.debug(' selecting model=%s, member=%s', opts.model_name, m)
df_anomaly_mean_cycle, df_anomaly_yearly = Multiset.get_anomaly_dataframes(ds.sel(member_id=m),
varname='co2')
df_anomaly_yearly['member_id'] = m
df_anomaly_mean_cycle['member_id'] = m
df_list_of_means.append(df_anomaly_mean_cycle)
df_list_of_yearly_cycles.append(df_anomaly_yearly)
df_anomaly_mean_cycle = pd.concat(df_list_of_means).groupby(['moy', 'plev']).mean().reset_index()
df_anomaly_yearly = pd.concat(df_list_of_yearly_cycles).groupby('moy').mean()
else:
raise ValueError('Unexpected case for member_key == <%s>' % opts.member_key)
if df_anomaly_yearly.empty:
# TODO: remove this bandaid if-statement.
warnings.warn("yearly anomaly is empty. plotting is skipped.", RuntimeWarning)
return new_self
# --- Plotting ---
fig, ax, bbox_artists = plot_annual_series(df_anomaly_yearly, df_anomaly_mean_cycle,
titlestr=f"{opts.model_name} ({opts.member_key})")
ax.text(0.02, 0.06, f"Global, surface level mean",
horizontalalignment='left', verticalalignment='center', transform=ax.transAxes)
if opts.figure_savepath:
mysavefig(fig=fig, plot_save_name=append_before_extension(opts.figure_savepath, 'cmip_annual_series'),
bbox_extra_artists=bbox_artists)
return new_self
def _load_data(self,
method: str = '',
url: str = default_cmip6_datastore_url,
model_name: Sequence[str] = None
) -> None:
"""
Parameters
----------
method : str
either 'pangeo' or 'local'
url : str
(Optional) only used if argument method=='pangeo'
model_name : str
"""
if model_name:
if isinstance(model_name, str):
model_name = [model_name]
if method == 'pangeo':
# --- Search for datasets in ESM data catalog ---
_logger.debug(' Opening the ESM datastore catalog, using URL == %s', url)
self.catalog_dataframe = intake.open_esm_datastore(url)
"""
We use the intake package's search to load a catalog into the attribute "latest_searched_model_catalog"
Acceptable search arguments include:
experiment_id
table_id
variable_id
institution_id
member_id
grid_label
"""
search_parameters = {'experiment_id': 'esm-hist',
'table_id': ['Amon'],
'variable_id': ['co2']}
_logger.debug(' Searching for model output subset, with parameters = %s', search_parameters)
self.latest_searched_model_catalog = self.catalog_dataframe.search(**search_parameters,
require_all_on=["source_id"])
_logger.debug(f" {self.latest_searched_model_catalog.df.shape[0]} model members identified")
# --- Load datasets into memory ---
_logger.debug(' Loading into memory the following models: %s', model_name)
self.stepA_original_datasets = DatasetDict(
self.latest_searched_model_catalog.to_dataset_dict(progressbar=self._progressbar))
# Extract all (or only the specified) datasets, and create a copy of each.
if not model_name:
model_name = self.stepA_original_datasets.keys()
self.stepA_original_datasets = DatasetDict({k: self.stepA_original_datasets[k] for k in model_name})
if method == 'local':
# A configuration object (for holding paths and settings) is read in to get the path to the data.
config = load_config_file()
cmip_data_path = config.get('CMIP', 'source', vars=os.environ)
_logger.debug(f"Loading local CMIP output files from path <{cmip_data_path}>..")
# NetCDF files are loaded. Each model has its own DatasetDict key.
dd = DatasetDict()
# model_shortnames = ['MPI-ESM.esm-hist', 'BCC.esm-hist']
for mdl_name in model_name:
_logger.debug(f"mdl_name = {mdl_name}")
mdl_name_dict = model_name_dict_from_valid_form(mdl_name)
constructed_model_name = os.path.normpath(f"{cmip_data_path}/*{mdl_name_dict['sourceid']}*{mdl_name_dict['experimentid']}*.nc")
_logger.debug(f" loading -- {constructed_model_name} --")
ds = xr.open_mfdataset(constructed_model_name, decode_times=True)
key = matched_model_and_experiment(ds.attrs['parent_source_id'] + '.' + ds.attrs['experiment_id'])
dd[key] = ds
self.stepA_original_datasets = dd
def preprocess(self) -> None:
"""Set up the datasets that are common to every diagnostic
"""
msg = "Preprocessing..."
msg += "\nConverting units to ppm.."
_logger.debug(msg)
self.stepB_preprocessed_datasets = self.stepA_original_datasets.copy()
self.stepB_preprocessed_datasets.apply_function_to_all(co2_molfrac_to_ppm, co2_var_name='co2', inplace=True)
self.stepB_preprocessed_datasets.apply_function_to_all(ensure_dataset_datetime64, inplace=True)
_logger.debug(" the first DataSet has a time range of <%s> to <%s>.",
np.datetime_as_string(list(self.stepB_preprocessed_datasets.items())[0][1]['time'].values[0], unit='D'),
np.datetime_as_string(list(self.stepB_preprocessed_datasets.items())[0][1]['time'].values[-1], unit='D'))
msg = "all converted."
msg += "\nKeys for models that have been preprocessed:"
msg += '\n ' + '\n '.join(self.stepB_preprocessed_datasets.keys())
msg += "\nPreprocessing is done."
_logger.debug(msg)
def plot_timeseries(self) -> (plt.Figure, plt.Axes, tuple):
"""Make timeseries plot of co2 concentrations from or more CMIP models
Requires self.stepC_prepped_datasets attribute with a time dimension.
Returns
-------
matplotlib figure
matplotlib axis
tuple
Extra matplotlib artists used for the bounding box (bbox) when saving a figure
"""
nmodels, member_counts = self._count_members()
my_cmap = self.categorical_cmap(nc=len(member_counts), nsc=max(member_counts), cmap="tab10")
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 4))
for ki, k in enumerate(self.stepC_prepped_datasets.keys()):
for mi, m in enumerate(self.stepC_prepped_datasets[k]['member_id'].values.tolist()):
color_count = ki * max(member_counts) + mi
data = self.stepC_prepped_datasets[k].sel(member_id=m)
data = ensure_dataset_datetime64(data)
y = data['co2'].squeeze()
ax.plot(y['time'], y, label=f"{k} ({m})",
color=my_cmap.colors[color_count], alpha=0.6)
ax.set_ylabel('ppm')
aesthetic_grid_no_spines(ax)
#
leg = plt.legend(title='Models', frameon=False,
bbox_to_anchor=(1.05, 1), loc='upper left',
fontsize=12)
bbox_artists = (leg,)
return fig, ax, bbox_artists
def plot_vertical_profiles(self) -> (plt.Figure, plt.Axes, tuple):
"""Make vertical profile plot of co2 concentrations.
Returns
-------
matplotlib figure
matplotlib axis
tuple
Extra matplotlib artists used for the bounding box (bbox) when saving a figure
"""
nmodels, member_counts = self._count_members()
my_cmap = self.categorical_cmap(nc=len(member_counts), nsc=max(member_counts), cmap="tab10")
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6))
for ki, k in enumerate(self.stepC_prepped_datasets.keys()):
for mi, m in enumerate(self.stepC_prepped_datasets[k]['member_id'].values.tolist()):
color_count = ki * max(member_counts) + mi
darray = self.stepC_prepped_datasets[k].sel(member_id=m)
ax.plot(darray['co2'].squeeze(), darray['plev'].squeeze(), label=f"{k} ({m})",
marker='o', linestyle='-',
color=my_cmap.colors[color_count], alpha=0.6)
ax.invert_yaxis()
ax.set_xlabel('ppm')
ax.set_ylabel('pressure [Pa]')
aesthetic_grid_no_spines(ax)
leg = plt.legend(title='Models', frameon=False,
bbox_to_anchor=(1.05, 1), loc='upper left',
fontsize=12)
bbox_artists = (leg,)
return fig, ax, bbox_artists
def __repr__(self) -> str:
"""String representation is built."""
nmodels, member_counts = self._count_members(suppress_log_message=True)
strrep = f"-- CMIP Collection -- \n" \
f"Datasets:" \
f"\n\t" + \
self._original_datasets_list_str() + \
f"\n" + \
f"There are <{member_counts}> members for each of the {nmodels} models." \
f"\n" \
f"All attributes:" \
f"\n\t" + \
'\n\t'.join(self._obj_attributes_list_str())
return strrep
def _count_members(self, suppress_log_message: bool = False):
"""Get the number of member_id values present for each model's dataset
Parameters
----------
suppress_log_message : bool, default False
Returns
-------
Tuple
The number of models (int) and the number of members for each model (list of int)
"""
if self.stepA_original_datasets:
ds_to_check = self.stepA_original_datasets
elif self.stepB_preprocessed_datasets:
ds_to_check = self.stepB_preprocessed_datasets
elif self.stepC_prepped_datasets:
ds_to_check = self.stepC_prepped_datasets
else:
return 0, 0
member_counts = []
for k in ds_to_check.keys():
member_counts.append(len(ds_to_check[k]['member_id'].values))
nmodels = len(member_counts)
if not suppress_log_message:
_logger.info(f"There are <%s> members for each of the %d models.", member_counts, nmodels)
return nmodels, member_counts
def add_cmip_collection_args_to_parser(parser: argparse.ArgumentParser) -> None:
"""Add recipe arguments to a parser object
Parameters
----------
parser : argparse.ArgumentParser
"""
add_shared_arguments_for_recipes(parser)
parser.add_argument('--plev', default=100000, type=int)
parser.add_argument('--model_name', default=None, type=matched_model_and_experiment, choices=cmip_model_choices)
parser.add_argument('--member_key', default=None, type=nullable_str)
parser.add_argument('--cmip_load_method', default='pangeo',
type=str, choices=['pangeo', 'local'])
def cmip_recipe_basics(func):
"""A decorator for starting a cmip recipe
"""
def parse_and_run(*args, **kwargs):
set_verbose(_logger, kwargs.get('verbose'))
opts = parse_recipe_options(kwargs.get('options'), add_cmip_collection_args_to_parser)
# Recipe is run.
returnval = func(*args, **kwargs)
return returnval
return parse_and_run
Functions
def add_cmip_collection_args_to_parser(parser: argparse.ArgumentParser) -> NoneType-
Add recipe arguments to a parser object
Parameters
parser:argparse.ArgumentParser
Expand source code
def add_cmip_collection_args_to_parser(parser: argparse.ArgumentParser) -> None: """Add recipe arguments to a parser object Parameters ---------- parser : argparse.ArgumentParser """ add_shared_arguments_for_recipes(parser) parser.add_argument('--plev', default=100000, type=int) parser.add_argument('--model_name', default=None, type=matched_model_and_experiment, choices=cmip_model_choices) parser.add_argument('--member_key', default=None, type=nullable_str) parser.add_argument('--cmip_load_method', default='pangeo', type=str, choices=['pangeo', 'local']) def cmip_recipe_basics(func)-
A decorator for starting a cmip recipe
Expand source code
def cmip_recipe_basics(func): """A decorator for starting a cmip recipe """ def parse_and_run(*args, **kwargs): set_verbose(_logger, kwargs.get('verbose')) opts = parse_recipe_options(kwargs.get('options'), add_cmip_collection_args_to_parser) # Recipe is run. returnval = func(*args, **kwargs) return returnval return parse_and_run
Classes
class Collection (datastore='cmip6', verbose: Union[bool, str] = False)-
Work simultaneous with multiple, consistent xarray Datasets.
Instantiate a CMIP Collection object.
Parameters
datastore:str- a shortened name of an ESM catalog that we will query for model outputs.
verbose:Union[bool, str]- can be either True, False, or a string for level such as "INFO, DEBUG, etc."
Expand source code
class Collection(Multiset): def __init__(self, datastore='cmip6', verbose: Union[bool, str] = False): """Instantiate a CMIP Collection object. Parameters ---------- datastore : str a shortened name of an ESM catalog that we will query for model outputs. verbose : Union[bool, str] can be either True, False, or a string for level such as "INFO, DEBUG, etc." """ # Set up the level of verbosity, i.e. how many log messages are displayed. set_verbose(_logger, verbose) self._progressbar = True if _logger.level > 10: # 10 is debug, 20 is info, etc. self._progressbar = False self.latest_searched_model_catalog = None self.catalog_dataframe = None # TODO: add capability to handle additional datastores besides cmip6, such as CMIP5, etc. if datastore == 'cmip6': self.datastore_url = default_cmip6_datastore_url else: raise ValueError('Unexpected/unhandled datastore <%s>', datastore) super().__init__(verbose=verbose) @classmethod def _recipe_base(cls, datastore='cmip6', verbose: Union[bool, str] = False, load_method: str = 'pangeo', pickle_file: str = None, skip_selections: bool = False, selection: dict = None, mean_dims: tuple = None, model_name: Union[str, list] = None ) -> ('Collection', bool): """Create an instance, and either preprocess or load already processed data. Parameters ---------- datastore : str, default 'cmip6' verbose : Union[bool, str], default False load_method : str, default 'pangeo' either 'pangeo', 'local', or 'pickle' pickle_file : str (Optional) path to a saved pickle file is used if argument load_method=='pickle selection : dict mean_dims : tuple Returns ------- tuple : (Collection, bool) Collection bool Whether datasets were loaded from a pickle file or not. (If not, there is probably more processing needed.) """ # An empty instance is created. new_self = cls(datastore=datastore, verbose=verbose) _logger.debug(' skip_selections: %s', skip_selections) loaded_from_pickle_bool = False if load_method == 'pickle': # If a valid filename is provided, datasets are loaded into stepC attribute and this is True, # otherwise, this is False. loaded_from_pickle_bool = new_self.datasets_from_pickle(filename=pickle_file, replace=True) _logger.debug(' loaded from pickle? --> %s', loaded_from_pickle_bool) else: # Data are formatted into the basic data structure common to various diagnostics. new_self._load_data(method=load_method, url=new_self.datastore_url, model_name=model_name) new_self.preprocess() if not skip_selections: _logger.debug(' applying selected bounds: %s', selection) new_self.stepC_prepped_datasets = new_self.stepB_preprocessed_datasets.queue_selection(**selection, inplace=False) # Spatial mean is calculated, leaving us with a time series. new_self.stepC_prepped_datasets.queue_mean(dim=mean_dims, inplace=True) # The lazily loaded selections and computations are here actually processed. new_self.stepC_prepped_datasets.execute_all(inplace=True) return new_self, loaded_from_pickle_bool @classmethod @benchmark_recipe def run_recipe_for_timeseries(cls, datastore='cmip6', verbose: Union[bool, str] = False, pickle_file: str = None, options: dict = None ) -> 'Collection': """Execute a series of preprocessing steps and generate a diagnostic result. Parameters ---------- datastore : str, default 'cmip6' verbose : Union[bool, str] can be either True, False, or a string for level such as "INFO, DEBUG, etc." pickle_file : str path to pickled datastore options A dictionary with zero or more of these parameter keys: start_yr : str, default '1960' end_yr : str, default None plev : int, default 100000 Returns ------- Collection object for CMIP6 that was used to generate the diagnostic """ set_verbose(_logger, verbose) opts = parse_recipe_options(options, add_cmip_collection_args_to_parser) # Apply diagnostic options and prep data for plotting selection = {'time': slice(opts.start_datetime, opts.end_datetime), 'plev': opts.plev} new_self, _ = cls._recipe_base(datastore=datastore, verbose=verbose, pickle_file=pickle_file, selection=selection, mean_dims=('lon', 'lat'), model_name=opts.model_name, load_method=opts.cmip_load_method) # --- Plotting --- fig, ax, bbox_artists = new_self.plot_timeseries() if opts.figure_savepath: mysavefig(fig=fig, plot_save_name=append_before_extension(opts.figure_savepath, 'cmip_timeseries'), bbox_extra_artists=bbox_artists) return new_self @classmethod @benchmark_recipe def run_recipe_for_vertical_profile(cls, datastore='cmip6', verbose: Union[bool, str] = False, pickle_file: str = None, options: dict = None ) -> 'Collection': """Execute a series of preprocessing steps and generate a diagnostic result. Parameters ---------- datastore : str verbose : Union[bool, str] can be either True, False, or a string for level such as "INFO, DEBUG, etc." pickle_file : str path to pickled datastore options A dictionary with zero or more of these parameter keys: start_yr : str, default '1960' end_yr : str, default None Returns ------- Collection object for CMIP6 that was used to generate the diagnostic """ set_verbose(_logger, verbose) opts = parse_recipe_options(options, add_cmip_collection_args_to_parser) # Apply diagnostic options and prep data for plotting selection = {'time': slice(opts.start_datetime, opts.end_datetime)} new_self, _ = cls._recipe_base(datastore=datastore, verbose=verbose, pickle_file=pickle_file, selection=selection, mean_dims=('lon', 'lat', 'time'), model_name=opts.model_name, load_method=opts.cmip_load_method) # --- Plotting --- fig, ax, bbox_artists = new_self.plot_vertical_profiles() if opts.figure_savepath: mysavefig(fig=fig, plot_save_name=append_before_extension(opts.figure_savepath, 'cmip_vertical_plot'), bbox_extra_artists=bbox_artists) return new_self @classmethod @benchmark_recipe def run_recipe_for_zonal_mean(cls, datastore='cmip6', verbose: Union[bool, str] = False, pickle_file: str = None, options: dict = None ) -> 'Collection': """Execute a series of preprocessing steps and generate a diagnostic result. Parameters ---------- datastore : str verbose : Union[bool, str] can be either True, False, or a string for level such as "INFO, DEBUG, etc." pickle_file : str path to pickled datastore options A dictionary with zero or more of these parameter keys: start_yr : str, default '1960' end_yr : str, default None Returns ------- Collection object for CMIP6 that was used to generate the diagnostic """ set_verbose(_logger, verbose) opts = parse_recipe_options(options, add_cmip_collection_args_to_parser) # Apply diagnostic options and prep data for plotting selection = {'time': slice(opts.start_datetime, opts.end_datetime)} new_self, _ = cls._recipe_base(datastore=datastore, verbose=verbose, pickle_file=pickle_file, selection=selection, mean_dims=('lon', 'time'), model_name=opts.model_name, load_method=opts.cmip_load_method) if not opts.member_key: _logger.debug("No 'member_key' supplied. Averaging over the available members: %s", new_self.stepC_prepped_datasets[opts.model_name]['member_id'].values.tolist()) opts.member_key = new_self.stepC_prepped_datasets[opts.model_name]['member_id'].values.tolist() # --- Plotting --- # darray = new_self.stepC_prepped_datasets[opts.model_name].sel(member_id=opts.member_key)['co2'] fig, ax, bbox_artists = plot_zonal_mean(darray, titlestr=f"{opts.model_name} ({opts.member_key})") if opts.figure_savepath: mysavefig(fig=fig, plot_save_name=append_before_extension(opts.figure_savepath, 'cmip_zonal_mean_plot'), bbox_extra_artists=bbox_artists) return new_self @classmethod @benchmark_recipe def run_recipe_for_annual_series(cls, datastore='cmip6', verbose: Union[bool, str] = False, pickle_file: str = None, options: dict = None ) -> 'Collection': """Execute a series of preprocessing steps and generate a diagnostic result. Parameters ---------- datastore : str verbose : Union[bool, str] can be either True, False, or a string for level such as "INFO, DEBUG, etc." pickle_file : str path to pickled datastore options A dictionary with zero or more of these parameter keys: start_yr : str, default '1960' end_yr : str, default None Returns ------- Collection object for CMIP6 that was used to generate the diagnostic """ set_verbose(_logger, verbose) opts = parse_recipe_options(options, add_cmip_collection_args_to_parser) # Apply diagnostic options and prep data for plotting selection = {'time': slice(opts.start_datetime, opts.end_datetime), 'plev': opts.plev} new_self, _ = cls._recipe_base(datastore=datastore, verbose=verbose, pickle_file=pickle_file, selection=selection, mean_dims=('lon', 'lat'), model_name=opts.model_name, load_method=opts.cmip_load_method) ds = new_self.stepC_prepped_datasets[opts.model_name] if not ('member_id' in ds.dims.keys()): df_anomaly_mean_cycle, df_anomaly_yearly = Multiset.get_anomaly_dataframes(ds, varname='co2') else: if not opts.member_key: _logger.debug("No 'member_key' supplied. Using the available member(s): %s", ds['member_id'].values.tolist()) opts.member_key = ds['member_id'].values.tolist() if isinstance(opts.member_key, str) or (len(opts.member_key) == 1): ds = new_self.stepC_prepped_datasets[opts.model_name].sel(member_id=opts.member_key) df_anomaly_mean_cycle, df_anomaly_yearly = Multiset.get_anomaly_dataframes(ds, varname='co2') elif isinstance(opts.member_key, list) and (len(opts.member_key) > 1): # The mean is calculated across ensemble members if there are multiple. df_list_of_means = [] df_list_of_yearly_cycles = [] for mi, m in enumerate(opts.member_key): _logger.debug(' selecting model=%s, member=%s', opts.model_name, m) df_anomaly_mean_cycle, df_anomaly_yearly = Multiset.get_anomaly_dataframes(ds.sel(member_id=m), varname='co2') df_anomaly_yearly['member_id'] = m df_anomaly_mean_cycle['member_id'] = m df_list_of_means.append(df_anomaly_mean_cycle) df_list_of_yearly_cycles.append(df_anomaly_yearly) df_anomaly_mean_cycle = pd.concat(df_list_of_means).groupby(['moy', 'plev']).mean().reset_index() df_anomaly_yearly = pd.concat(df_list_of_yearly_cycles).groupby('moy').mean() else: raise ValueError('Unexpected case for member_key == <%s>' % opts.member_key) if df_anomaly_yearly.empty: # TODO: remove this bandaid if-statement. warnings.warn("yearly anomaly is empty. plotting is skipped.", RuntimeWarning) return new_self # --- Plotting --- fig, ax, bbox_artists = plot_annual_series(df_anomaly_yearly, df_anomaly_mean_cycle, titlestr=f"{opts.model_name} ({opts.member_key})") ax.text(0.02, 0.06, f"Global, surface level mean", horizontalalignment='left', verticalalignment='center', transform=ax.transAxes) if opts.figure_savepath: mysavefig(fig=fig, plot_save_name=append_before_extension(opts.figure_savepath, 'cmip_annual_series'), bbox_extra_artists=bbox_artists) return new_self def _load_data(self, method: str = '', url: str = default_cmip6_datastore_url, model_name: Sequence[str] = None ) -> None: """ Parameters ---------- method : str either 'pangeo' or 'local' url : str (Optional) only used if argument method=='pangeo' model_name : str """ if model_name: if isinstance(model_name, str): model_name = [model_name] if method == 'pangeo': # --- Search for datasets in ESM data catalog --- _logger.debug(' Opening the ESM datastore catalog, using URL == %s', url) self.catalog_dataframe = intake.open_esm_datastore(url) """ We use the intake package's search to load a catalog into the attribute "latest_searched_model_catalog" Acceptable search arguments include: experiment_id table_id variable_id institution_id member_id grid_label """ search_parameters = {'experiment_id': 'esm-hist', 'table_id': ['Amon'], 'variable_id': ['co2']} _logger.debug(' Searching for model output subset, with parameters = %s', search_parameters) self.latest_searched_model_catalog = self.catalog_dataframe.search(**search_parameters, require_all_on=["source_id"]) _logger.debug(f" {self.latest_searched_model_catalog.df.shape[0]} model members identified") # --- Load datasets into memory --- _logger.debug(' Loading into memory the following models: %s', model_name) self.stepA_original_datasets = DatasetDict( self.latest_searched_model_catalog.to_dataset_dict(progressbar=self._progressbar)) # Extract all (or only the specified) datasets, and create a copy of each. if not model_name: model_name = self.stepA_original_datasets.keys() self.stepA_original_datasets = DatasetDict({k: self.stepA_original_datasets[k] for k in model_name}) if method == 'local': # A configuration object (for holding paths and settings) is read in to get the path to the data. config = load_config_file() cmip_data_path = config.get('CMIP', 'source', vars=os.environ) _logger.debug(f"Loading local CMIP output files from path <{cmip_data_path}>..") # NetCDF files are loaded. Each model has its own DatasetDict key. dd = DatasetDict() # model_shortnames = ['MPI-ESM.esm-hist', 'BCC.esm-hist'] for mdl_name in model_name: _logger.debug(f"mdl_name = {mdl_name}") mdl_name_dict = model_name_dict_from_valid_form(mdl_name) constructed_model_name = os.path.normpath(f"{cmip_data_path}/*{mdl_name_dict['sourceid']}*{mdl_name_dict['experimentid']}*.nc") _logger.debug(f" loading -- {constructed_model_name} --") ds = xr.open_mfdataset(constructed_model_name, decode_times=True) key = matched_model_and_experiment(ds.attrs['parent_source_id'] + '.' + ds.attrs['experiment_id']) dd[key] = ds self.stepA_original_datasets = dd def preprocess(self) -> None: """Set up the datasets that are common to every diagnostic """ msg = "Preprocessing..." msg += "\nConverting units to ppm.." _logger.debug(msg) self.stepB_preprocessed_datasets = self.stepA_original_datasets.copy() self.stepB_preprocessed_datasets.apply_function_to_all(co2_molfrac_to_ppm, co2_var_name='co2', inplace=True) self.stepB_preprocessed_datasets.apply_function_to_all(ensure_dataset_datetime64, inplace=True) _logger.debug(" the first DataSet has a time range of <%s> to <%s>.", np.datetime_as_string(list(self.stepB_preprocessed_datasets.items())[0][1]['time'].values[0], unit='D'), np.datetime_as_string(list(self.stepB_preprocessed_datasets.items())[0][1]['time'].values[-1], unit='D')) msg = "all converted." msg += "\nKeys for models that have been preprocessed:" msg += '\n ' + '\n '.join(self.stepB_preprocessed_datasets.keys()) msg += "\nPreprocessing is done." _logger.debug(msg) def plot_timeseries(self) -> (plt.Figure, plt.Axes, tuple): """Make timeseries plot of co2 concentrations from or more CMIP models Requires self.stepC_prepped_datasets attribute with a time dimension. Returns ------- matplotlib figure matplotlib axis tuple Extra matplotlib artists used for the bounding box (bbox) when saving a figure """ nmodels, member_counts = self._count_members() my_cmap = self.categorical_cmap(nc=len(member_counts), nsc=max(member_counts), cmap="tab10") fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 4)) for ki, k in enumerate(self.stepC_prepped_datasets.keys()): for mi, m in enumerate(self.stepC_prepped_datasets[k]['member_id'].values.tolist()): color_count = ki * max(member_counts) + mi data = self.stepC_prepped_datasets[k].sel(member_id=m) data = ensure_dataset_datetime64(data) y = data['co2'].squeeze() ax.plot(y['time'], y, label=f"{k} ({m})", color=my_cmap.colors[color_count], alpha=0.6) ax.set_ylabel('ppm') aesthetic_grid_no_spines(ax) # leg = plt.legend(title='Models', frameon=False, bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=12) bbox_artists = (leg,) return fig, ax, bbox_artists def plot_vertical_profiles(self) -> (plt.Figure, plt.Axes, tuple): """Make vertical profile plot of co2 concentrations. Returns ------- matplotlib figure matplotlib axis tuple Extra matplotlib artists used for the bounding box (bbox) when saving a figure """ nmodels, member_counts = self._count_members() my_cmap = self.categorical_cmap(nc=len(member_counts), nsc=max(member_counts), cmap="tab10") fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6)) for ki, k in enumerate(self.stepC_prepped_datasets.keys()): for mi, m in enumerate(self.stepC_prepped_datasets[k]['member_id'].values.tolist()): color_count = ki * max(member_counts) + mi darray = self.stepC_prepped_datasets[k].sel(member_id=m) ax.plot(darray['co2'].squeeze(), darray['plev'].squeeze(), label=f"{k} ({m})", marker='o', linestyle='-', color=my_cmap.colors[color_count], alpha=0.6) ax.invert_yaxis() ax.set_xlabel('ppm') ax.set_ylabel('pressure [Pa]') aesthetic_grid_no_spines(ax) leg = plt.legend(title='Models', frameon=False, bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=12) bbox_artists = (leg,) return fig, ax, bbox_artists def __repr__(self) -> str: """String representation is built.""" nmodels, member_counts = self._count_members(suppress_log_message=True) strrep = f"-- CMIP Collection -- \n" \ f"Datasets:" \ f"\n\t" + \ self._original_datasets_list_str() + \ f"\n" + \ f"There are <{member_counts}> members for each of the {nmodels} models." \ f"\n" \ f"All attributes:" \ f"\n\t" + \ '\n\t'.join(self._obj_attributes_list_str()) return strrep def _count_members(self, suppress_log_message: bool = False): """Get the number of member_id values present for each model's dataset Parameters ---------- suppress_log_message : bool, default False Returns ------- Tuple The number of models (int) and the number of members for each model (list of int) """ if self.stepA_original_datasets: ds_to_check = self.stepA_original_datasets elif self.stepB_preprocessed_datasets: ds_to_check = self.stepB_preprocessed_datasets elif self.stepC_prepped_datasets: ds_to_check = self.stepC_prepped_datasets else: return 0, 0 member_counts = [] for k in ds_to_check.keys(): member_counts.append(len(ds_to_check[k]['member_id'].values)) nmodels = len(member_counts) if not suppress_log_message: _logger.info(f"There are <%s> members for each of the %d models.", member_counts, nmodels) return nmodels, member_countsAncestors
Static methods
def run_recipe_for_annual_series(*args, **kwargs)-
Expand source code
def display_time_and_call(*args, **kwargs): # Clock is started. start_time = time.time() # Recipe is run. returnval = func(*args, **kwargs) # Report the time this recipe took to execute. execution_time = (time.time() - start_time) _logger.info('recipe execution time (seconds): ' + str(execution_time)) return returnval def run_recipe_for_timeseries(*args, **kwargs)-
Expand source code
def display_time_and_call(*args, **kwargs): # Clock is started. start_time = time.time() # Recipe is run. returnval = func(*args, **kwargs) # Report the time this recipe took to execute. execution_time = (time.time() - start_time) _logger.info('recipe execution time (seconds): ' + str(execution_time)) return returnval def run_recipe_for_vertical_profile(*args, **kwargs)-
Expand source code
def display_time_and_call(*args, **kwargs): # Clock is started. start_time = time.time() # Recipe is run. returnval = func(*args, **kwargs) # Report the time this recipe took to execute. execution_time = (time.time() - start_time) _logger.info('recipe execution time (seconds): ' + str(execution_time)) return returnval def run_recipe_for_zonal_mean(*args, **kwargs)-
Expand source code
def display_time_and_call(*args, **kwargs): # Clock is started. start_time = time.time() # Recipe is run. returnval = func(*args, **kwargs) # Report the time this recipe took to execute. execution_time = (time.time() - start_time) _logger.info('recipe execution time (seconds): ' + str(execution_time)) return returnval
Methods
def plot_timeseries(self) -> (, , ) -
Make timeseries plot of co2 concentrations from or more CMIP models
Requires self.stepC_prepped_datasets attribute with a time dimension.
Returns
matplotlib figurematplotlib axistuple- Extra matplotlib artists used for the bounding box (bbox) when saving a figure
Expand source code
def plot_timeseries(self) -> (plt.Figure, plt.Axes, tuple): """Make timeseries plot of co2 concentrations from or more CMIP models Requires self.stepC_prepped_datasets attribute with a time dimension. Returns ------- matplotlib figure matplotlib axis tuple Extra matplotlib artists used for the bounding box (bbox) when saving a figure """ nmodels, member_counts = self._count_members() my_cmap = self.categorical_cmap(nc=len(member_counts), nsc=max(member_counts), cmap="tab10") fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 4)) for ki, k in enumerate(self.stepC_prepped_datasets.keys()): for mi, m in enumerate(self.stepC_prepped_datasets[k]['member_id'].values.tolist()): color_count = ki * max(member_counts) + mi data = self.stepC_prepped_datasets[k].sel(member_id=m) data = ensure_dataset_datetime64(data) y = data['co2'].squeeze() ax.plot(y['time'], y, label=f"{k} ({m})", color=my_cmap.colors[color_count], alpha=0.6) ax.set_ylabel('ppm') aesthetic_grid_no_spines(ax) # leg = plt.legend(title='Models', frameon=False, bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=12) bbox_artists = (leg,) return fig, ax, bbox_artists def plot_vertical_profiles(self) -> (, , ) -
Make vertical profile plot of co2 concentrations.
Returns
matplotlib figurematplotlib axistuple- Extra matplotlib artists used for the bounding box (bbox) when saving a figure
Expand source code
def plot_vertical_profiles(self) -> (plt.Figure, plt.Axes, tuple): """Make vertical profile plot of co2 concentrations. Returns ------- matplotlib figure matplotlib axis tuple Extra matplotlib artists used for the bounding box (bbox) when saving a figure """ nmodels, member_counts = self._count_members() my_cmap = self.categorical_cmap(nc=len(member_counts), nsc=max(member_counts), cmap="tab10") fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6)) for ki, k in enumerate(self.stepC_prepped_datasets.keys()): for mi, m in enumerate(self.stepC_prepped_datasets[k]['member_id'].values.tolist()): color_count = ki * max(member_counts) + mi darray = self.stepC_prepped_datasets[k].sel(member_id=m) ax.plot(darray['co2'].squeeze(), darray['plev'].squeeze(), label=f"{k} ({m})", marker='o', linestyle='-', color=my_cmap.colors[color_count], alpha=0.6) ax.invert_yaxis() ax.set_xlabel('ppm') ax.set_ylabel('pressure [Pa]') aesthetic_grid_no_spines(ax) leg = plt.legend(title='Models', frameon=False, bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=12) bbox_artists = (leg,) return fig, ax, bbox_artists def preprocess(self) -> NoneType-
Set up the datasets that are common to every diagnostic
Expand source code
def preprocess(self) -> None: """Set up the datasets that are common to every diagnostic """ msg = "Preprocessing..." msg += "\nConverting units to ppm.." _logger.debug(msg) self.stepB_preprocessed_datasets = self.stepA_original_datasets.copy() self.stepB_preprocessed_datasets.apply_function_to_all(co2_molfrac_to_ppm, co2_var_name='co2', inplace=True) self.stepB_preprocessed_datasets.apply_function_to_all(ensure_dataset_datetime64, inplace=True) _logger.debug(" the first DataSet has a time range of <%s> to <%s>.", np.datetime_as_string(list(self.stepB_preprocessed_datasets.items())[0][1]['time'].values[0], unit='D'), np.datetime_as_string(list(self.stepB_preprocessed_datasets.items())[0][1]['time'].values[-1], unit='D')) msg = "all converted." msg += "\nKeys for models that have been preprocessed:" msg += '\n ' + '\n '.join(self.stepB_preprocessed_datasets.keys()) msg += "\nPreprocessing is done." _logger.debug(msg)
Inherited members