Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADD: plotting options to analysis, regional fixes #12

Merged
merged 12 commits into from
Jan 24, 2025
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 @@
----------
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 @@
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 @@
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 @@
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 @@
]
)
# 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 @@
)
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

Check warning on line 312 in ilamb3/analysis/bias.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/analysis/bias.py#L311-L312

Added lines #L311 - L312 were not covered by tests

# 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"]

Check warning on line 318 in ilamb3/analysis/bias.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/analysis/bias.py#L315-L318

Added lines #L315 - L318 were not covered by tests

# Build up a dataframe of matplotlib axes
axs = [

Check warning on line 321 in ilamb3/analysis/bias.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/analysis/bias.py#L321

Added line #L321 was not covered by tests
{
"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"])

Check warning on line 344 in ilamb3/analysis/bias.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/analysis/bias.py#L344

Added line #L344 was not covered by tests

return axs

Check warning on line 346 in ilamb3/analysis/bias.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/analysis/bias.py#L346

Added line #L346 was not covered by tests
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))

Check warning on line 32 in ilamb3/config.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/config.py#L32

Added line #L32 was not covered by tests

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)

Check warning on line 44 in ilamb3/config.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/config.py#L41-L44

Added lines #L41 - L44 were not covered by tests

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

Check warning on line 50 in ilamb3/config.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/config.py#L48-L50

Added lines #L48 - L50 were not covered by tests

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(

Check warning on line 66 in ilamb3/config.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/config.py#L59-L66

Added lines #L59 - L66 were not covered by tests
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)

Check warning on line 70 in ilamb3/config.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/config.py#L69-L70

Added lines #L69 - L70 were not covered by tests

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

Check warning on line 76 in ilamb3/config.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/config.py#L73-L76

Added lines #L73 - L76 were not covered by tests
else:
raise KeyError(item)

Check warning on line 78 in ilamb3/config.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/config.py#L78

Added line #L78 was not covered by tests

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

Check warning on line 83 in ilamb3/config.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/config.py#L81-L83

Added lines #L81 - L83 were not covered by tests

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

Check warning on line 97 in ilamb3/config.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/config.py#L93-L97

Added lines #L93 - L97 were not covered by tests


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 @@
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 @@
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

Check warning on line 127 in ilamb3/dataset.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/dataset.py#L122-L127

Added lines #L122 - L127 were not covered by tests


def is_spatial(da: xr.DataArray) -> bool:
"""
Return if the dataarray is spatial.
Expand Down Expand Up @@ -528,7 +550,7 @@
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 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

Check warning on line 54 in ilamb3/exceptions.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/exceptions.py#L51-L54

Added lines #L51 - L54 were not covered by tests

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

Check warning on line 57 in ilamb3/exceptions.py

View check run for this annotation

Codecov / codecov/patch

ilamb3/exceptions.py#L57

Added line #L57 was not covered by tests
Loading
Loading