def test_merge_attributes(missing_str, new_line): a = xr.DataArray([0], attrs={"text": "Text1"}, name="a") b = xr.DataArray([0], attrs={}) c = xr.Dataset(attrs={"text": "Text3"}) merged = merge_attributes( "text", a, missing_str=missing_str, new_line=new_line, b=b, c=c ) assert merged.startswith("a: Text1") if missing_str is not None: assert merged.count(new_line) == 2 assert f"b: {missing_str}" in merged else: assert merged.count(new_line) == 1 assert "b:" not in merged
def parametric_quantile(p: xr.DataArray, q: Union[int, Sequence]) -> xr.DataArray: """Return the value corresponding to the given distribution parameters and quantile. Parameters ---------- p : xr.DataArray Distribution parameters returned by the `fit` function. The array should have dimension `dparams` storing the distribution parameters, and attribute `scipy_dist`, storing the name of the distribution. q : Union[float, Sequence] Quantile to compute, which must be between `0` and `1`, inclusive. Returns ------- xarray.DataArray An array of parametric quantiles estimated from the distribution parameters. Notes ----- When all quantiles are above 0.5, the `isf` method is used instead of `ppf` because accuracy is sometimes better. """ q = np.atleast_1d(q) # Get the distribution dist = p.attrs["scipy_dist"] dc = get_dist(dist) # Create a lambda function to facilitate passing arguments to dask. There is probably a better way to do this. if np.all(q > 0.5): def func(x): return dc.isf(1 - q, *x) else: def func(x): return dc.ppf(q, *x) duck = dask.array if isinstance(p.data, dask.array.Array) else np data = duck.apply_along_axis(func, p.get_axis_num("dparams"), p) # Create coordinate for the return periods coords = dict(p.coords.items()) coords.pop("dparams") coords["quantile"] = q # Create dimensions dims = [d if d != "dparams" else "quantile" for d in p.dims] out = xr.DataArray(data=data, coords=coords, dims=dims) out.attrs = unprefix_attrs(p.attrs, ["units", "standard_name"], "original_") attrs = dict( long_name=f"{dist} quantiles", description=f"Quantiles estimated by the {dist} distribution", cell_methods=merge_attributes("dparams: ppf", out, new_line=" "), xclim_history=update_history( "Compute parametric quantiles from distribution parameters", new_name="parametric_quantile", parameters=p, ), ) out.attrs.update(attrs) return out
def __call__(self, *args, **kwds): # Bind call arguments. We need to use the class signature, not the instance, otherwise it removes the first # argument. ba = self._sig.bind(*args, **kwds) ba.apply_defaults() # Update attributes out_attrs = self.format(self.cf_attrs, ba.arguments) for locale in LOCALES: out_attrs.update( self.format( get_local_attrs( self, locale, names=self._cf_names, fill_missing=False, append_locale_name=True, ), args=ba.arguments, formatter=get_local_formatter(locale), )) vname = self.format({"var_name": self.var_name}, ba.arguments)["var_name"] # Update the signature with the values of the actual call. cp = OrderedDict() for (k, v) in ba.signature.parameters.items(): if v.default is not None and isinstance(v.default, (float, int, str)): cp[k] = v.replace(default=ba.arguments[k]) else: cp[k] = v # Assume the first arguments are always the DataArray. das = OrderedDict() for i in range(self._nvar): das[self._parameters[i]] = ba.arguments.pop(self._parameters[i]) # Get history and cell method attributes from source data attrs = defaultdict(str) attrs["cell_methods"] = merge_attributes("cell_methods", new_line=" ", missing_str=None, **das) if "cell_methods" in out_attrs: attrs["cell_methods"] += " " + out_attrs.pop("cell_methods") attrs["history"] = update_history( f"{self.identifier}{ba.signature.replace(parameters=cp.values())}", new_name=vname, **das, ) attrs.update(out_attrs) # Pre-computation validation checks for da in das.values(): self.validate(da) self.cfprobe(*das.values()) # Compute the indicator values, ignoring NaNs. out = self.compute(**das, **ba.kwargs) # Convert to output units out = convert_units_to(out, self.units, self.context) # Update netCDF attributes out.attrs.update(attrs) # Bind call arguments to the `missing` function, whose signature might be different from `compute`. mba = signature(self.missing).bind(*das.values(), **ba.arguments) # Mask results that do not meet criteria defined by the `missing` method. mask = self.missing(*mba.args, **mba.kwargs) ma_out = out.where(~mask) return ma_out.rename(vname)