Skip to content

Commit

Permalink
ADD: plotting options to analysis, regional fixes (#12)
Browse files Browse the repository at this point in the history
* embed the post in try/except blocks

* add titles to figures

* analysis plots populate dynamically

* fix updates to images

* regional analysis works

* removing unwanted capability

* fix failing tests
  • Loading branch information
nocollier authored Jan 24, 2025
1 parent e07a023 commit 1c32151
Show file tree
Hide file tree
Showing 11 changed files with 748 additions and 86 deletions.
3 changes: 2 additions & 1 deletion ilamb3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from cf_xarray.units import units

from ilamb3._version import __version__ # noqa
from ilamb3.config import conf # noqa

# additional units that pint/cf-xarray does not handle
units.define("kg = 1e3 * g")
Expand Down Expand Up @@ -42,5 +43,5 @@ def ilamb_catalog() -> pooch.Pooch:
return registry


__all__ = ["dataset", "compare", "analysis", "regions", "ilamb_catalog"]
__all__ = ["dataset", "compare", "analysis", "regions", "ilamb_catalog,", "conf"]
xr.set_options(keep_attrs=True)
58 changes: 54 additions & 4 deletions ilamb3/analysis/bias.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import pandas as pd
import xarray as xr

import ilamb3.plot as plt
from ilamb3 import compare as cmp
from ilamb3 import dataset as dset
from ilamb3.analysis.base import ILAMBAnalysis
Expand All @@ -28,6 +29,8 @@ class bias_analysis(ILAMBAnalysis):
----------
required_variable : str
The name of the variable to be used in this analysis.
variable_cmap : str
The colormap to use in plots of the comparison variable, optional.
Methods
-------
Expand All @@ -37,8 +40,11 @@ class bias_analysis(ILAMBAnalysis):
The method
"""

def __init__(self, required_variable: str): # numpydoc ignore=GL08
def __init__(
self, required_variable: str, variable_cmap: str = "viridis"
): # numpydoc ignore=GL08
self.req_variable = required_variable
self.cmap = variable_cmap

def required_variables(self) -> list[str]:
"""
Expand Down Expand Up @@ -108,7 +114,6 @@ def __call__(
varname = self.req_variable
if use_uncertainty and "bounds" not in ref[varname].attrs:
use_uncertainty = False

# Checks on the database if it is being used
if method == "RegionalQuantiles":
check_quantile_database(quantile_dbase)
Expand Down Expand Up @@ -195,7 +200,7 @@ def __call__(
if use_uncertainty:
ref_out["uncert"] = uncert
com_out = bias.to_dataset(name="bias")
com_out["bias_score"] = score
com_out["biasscore"] = score
try:
lat_name = dset.get_dim_name(com_mean, "lat")
lon_name = dset.get_dim_name(com_mean, "lon")
Expand Down Expand Up @@ -261,7 +266,7 @@ def _scalar(
]
)
# Bias Score
bias_scalar_score = _scalar(com_out, "bias_score", region, True, True)
bias_scalar_score = _scalar(com_out, "biasscore", region, True, True)
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore", "divide by zero encountered in divide", RuntimeWarning
Expand Down Expand Up @@ -294,3 +299,48 @@ def _scalar(
)
dfs.attrs = dict(method=method)
return dfs, ref_out, com_out

def plots(
self,
df: pd.DataFrame,
ref: xr.Dataset,
com: dict[str, xr.Dataset],
) -> pd.DataFrame:

# Some initialization
regions = [None if r == "None" else r for r in df["region"].unique()]
com["Reference"] = ref

# Setup plot data
df = plt.determine_plot_limits(com).set_index("name")
df.loc["mean", ["cmap", "title"]] = [self.cmap, "Period Mean"]
df.loc["bias", ["cmap", "title"]] = ["seismic", "Bias"]
df.loc["biasscore", ["cmap", "title"]] = ["plasma", "Bias Score"]

# Build up a dataframe of matplotlib axes
axs = [
{
"name": plot,
"title": df.loc[plot, "title"],
"region": region,
"source": source,
"axis": (
plt.plot_map(
ds[plot],
region=region,
vmin=df.loc[plot, "low"],
vmax=df.loc[plot, "high"],
cmap=df.loc[plot, "cmap"],
title=source + " " + df.loc[plot, "title"],
)
if plot in ds
else pd.NA
),
}
for plot in ["mean", "bias", "biasscore"]
for source, ds in com.items()
for region in regions
]
axs = pd.DataFrame(axs).dropna(subset=["axis"])

return axs
2 changes: 1 addition & 1 deletion ilamb3/analysis/relationship.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ def __call__(
analysis_name,
f"Score {var_dep} vs {var_ind}",
"score",
"",
"1",
score,
]
)
Expand Down
101 changes: 101 additions & 0 deletions ilamb3/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Configuration for ilamb3"""

import contextlib
import copy
from pathlib import Path

import yaml

import ilamb3.regions as reg

defaults = {
"build_dir": "./_build",
"regions": [None],
}


class Config(dict):
"""A global configuration object used in the package."""

def __init__(self, filename: Path | None = None, **kwargs):
self.filename = (
Path(filename)
if filename is not None
else Path.home() / ".config/ilamb3/conf.yaml"
)
self.filename.parent.mkdir(parents=True, exist_ok=True)
self.reload_all()
self.temp = None
super().__init__(**kwargs)

def __repr__(self):
return yaml.dump(dict(self))

def reset(self):
"""Return to defaults."""
self.clear()
self.update(copy.deepcopy(defaults))

def save(self, filename: Path | None = None):
"""Save current configuration to file as YAML."""
filename = filename or self.filename
filename.parent.mkdir(parents=True, exist_ok=True)
with open(filename, "w") as f:
yaml.dump(dict(self), f)

@contextlib.contextmanager
def _unset(self, temp):
yield
self.clear()
self.update(temp)

def set(
self,
*,
build_dir: str | None = None,
regions: list[str] | None = None,
):
"""Change ilamb3 configuration options."""
temp = copy.deepcopy(self)
if build_dir is not None:
self["build_dir"] = str(build_dir)
if regions is not None:
ilamb_regions = reg.Regions()
does_not_exist = set(regions) - set(ilamb_regions._regions) - set([None])
if does_not_exist:
raise ValueError(
f"Cannot run ILAMB over these regions [{list(does_not_exist)}] which are not registered in our system [{list(ilamb_regions._regions)}]"
)
self["regions"] = regions
return self._unset(temp)

def __getitem__(self, item):
if item in self:
return super().__getitem__(item)
elif item in defaults:
return defaults[item]
else:
raise KeyError(item)

def get(self, key, default=None):
if key in self:
return super().__getitem__(key)
return default

def reload_all(self):
self.reset()
self.load()

def load(self, filename: Path | None = None):
"""Update global config from YAML file or default file if None."""
filename = filename or self.filename
if filename.is_file():
with open(filename) as f:
try:
self.update(yaml.safe_load(f))
except Exception:
pass


conf = Config()
conf.reload_all()
30 changes: 26 additions & 4 deletions ilamb3/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import numpy as np
import xarray as xr

import ilamb3.regions as ilreg
from ilamb3.exceptions import NoSiteDimension
from ilamb3.regions import Regions


def get_dim_name(
Expand Down Expand Up @@ -93,8 +93,8 @@ def get_coord_name(
get_dim_name : A variant when the coordinate is a dimension.
"""
coord_names = {
"lat": ["lat", "latitude", "Latitude", "y"],
"lon": ["lon", "longitude", "Longitude", "x"],
"lat": ["lat", "latitude", "Latitude", "y", "lat_"],
"lon": ["lon", "longitude", "Longitude", "x", "lon_"],
}
possible_names = coord_names[coord]
coord_name = set(dset.coords).intersection(possible_names)
Expand All @@ -105,6 +105,28 @@ def get_coord_name(
return str(coord_name.pop())


def is_temporal(da: xr.DataArray) -> bool:
"""
Return if the dataarray is temporal.
Parameters
----------
da : xr.DataArray
The input dataarray.
Returns
-------
bool
True if time dimension is present, False otherwise.
"""
try:
get_dim_name(da, "time")
return True
except KeyError:
pass
return False


def is_spatial(da: xr.DataArray) -> bool:
"""
Return if the dataarray is spatial.
Expand Down Expand Up @@ -528,7 +550,7 @@ def integrate_space(
only be done in a single dimension at a time.
"""
if region is not None:
regions = Regions()
regions = ilreg.Regions()
dset = regions.restrict_to_region(dset, region)
space = [get_dim_name(dset, "lat"), get_dim_name(dset, "lon")]
if not isinstance(dset, xr.Dataset):
Expand Down
28 changes: 28 additions & 0 deletions ilamb3/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,31 @@ class NoDatabaseEntry(ILAMBException):

class NoSiteDimension(ILAMBException):
"""The dataset/dataarray does not contain a clear site dimension."""


class AnalysisFailure(ILAMBException):
"""
The analysis function you were running threw an exception.
Parameters
----------
analysis : str
The name of the analysis which has failed.
variable : str
The name of the variable associated with this analysis.
source : str
The name of the source of the reference data.
model : str
The name of the model whose analysis failed.
"""

def __init__(
self, analysis: str, variable: str, source: str, model: str
): # numpydoc ignore=GL08
self.analysis = analysis
self.variable = variable
self.source = source
self.model = model

def __str__(self): # numpydoc ignore=GL08
return f"The '{self.analysis}' analysis failed for '{self.variable} | {self.source}' against '{self.model}'"
Loading

0 comments on commit 1c32151

Please sign in to comment.