Source code for geoips.interfaces.class_based.coverage_checkers

# # # This source code is subject to the license referenced at
# # # https://github.com/NRLMMD-GEOIPS.

"""Coverage checkers interface class."""

import xarray as xr

from geoips.interfaces.base import BaseClassInterface
from geoips.interfaces.class_based_plugin import BaseClassPlugin
from geoips.utils.types.datatree_helpers import to_mutable_dataset


[docs]class BaseCoverageCheckerPlugin(BaseClassPlugin, abstract=True): """Base class for GeoIPS coverage_checker plugins.""" data_tree = False def _invoke(self, data=None, *args, _obp_initiated=False, **kwargs): """Iterate over ``variables`` when a list is given, then delegate. When *kwargs* contains a ``variables`` list (from step arguments) the coverage checker is invoked once per entry with ``variable_name`` injected. If ``minimum_coverage`` is also present, a ``ValueError`` is raised immediately when any variable's coverage falls below the threshold. Otherwise delegates to the standard ``_invoke`` without alteration. Parameters ---------- data : xr.DataTree or xr.Dataset or None Upstream data passed into the plugin. args : tuple Additional positional arguments forwarded to the base ``_invoke``. _obp_initiated : bool, default=False Whether the call originates from the Order-Based Procflow. kwargs : dict Step arguments. When ``variables`` is a ``list`` it is consumed along with ``minimum_coverage``; remaining keys are forwarded to each per-variable call. Returns ------- DataTreeDitto The output of the worst-coverage variable. Raises ------ ValueError When ``minimum_coverage`` is set and any variable's coverage percentage falls below it. """ if _obp_initiated and "variable_name" not in kwargs: kwargs["variable_name"] = self._infer_variable_name(data) variables = kwargs.get("variables") if not _obp_initiated or not isinstance(variables, list): return super()._invoke(data, *args, _obp_initiated=_obp_initiated, **kwargs) minimum_coverage = kwargs.pop("minimum_coverage", 10.0) kwargs.pop("variables") min_cov = 100.0 result = None for vname in variables: vkwargs = {**kwargs, "variable_name": vname} res = super()._invoke(data, *args, _obp_initiated=_obp_initiated, **vkwargs) cov = res.ds.attrs.get("coverage", res.ds.attrs.get("value", 0.0)) if cov < minimum_coverage: raise ValueError( f"Coverage for variable '{vname}' is {cov:.1f}%, " f"below the required minimum of " f"{minimum_coverage:.1f}%." ) if cov < min_cov: min_cov = cov result = res return result def _infer_variable_name(self, data): """Infer the coverage-checker variable from upstream data when unambiguous. Coverage checker steps may be inserted automatically while expanding legacy product definitions. In those cases, the product configuration often does not name the algorithm output variable. Use the actual upstream dataset at call time to infer the variable only when exactly one data variable is available. """ dataset = to_mutable_dataset(data) data_vars = list(getattr(dataset, "data_vars", [])) if len(data_vars) == 1: return data_vars[0] if not data_vars: raise ValueError( f"Coverage checker plugin '{self.name}' requires 'variable_name', " "but no data variables were found in the upstream data." ) raise ValueError( f"Coverage checker plugin '{self.name}' requires 'variable_name' when " "upstream data contains multiple data variables. Available variables: " f"{data_vars}." ) def _pre_call(self, data=None, *args, _obp_initiated=False, **kwargs): """Flatten OBP DataTree input into a mutable Dataset before base hooks. Mirrors ``BaseAlgorithmPlugin._pre_call``: under OBP upstream outputs arrive as a ``DataTree`` whose child nodes expose immutable ``DatasetView`` objects via ``.ds``. This override flattens that tree into a mutable Dataset so ``call()`` receives the expected input. Legacy (non-OBP) inputs pass through unchanged. """ if _obp_initiated and isinstance(data, xr.DataTree): data = to_mutable_dataset(data) return super()._pre_call(data, *args, _obp_initiated=_obp_initiated, **kwargs) def _post_call(self, data=None, *args, _obp_initiated=False, **kwargs): r"""Normalize coverage_checker output into ``DataTreeDitto``. The base ``_post_call`` wraps scalar outputs into ``DataTreeDitto`` and stores them under ``attrs["value"]``. This override renames that key to ``attrs["coverage"]`` so downstream steps can access coverage metadata unambiguously. Parameters ---------- data : float | Any, optional Coverage output produced by the plugin. Scalar values are wrapped by the base ``_post_call`` into ``DataTreeDitto``. \*args : tuple Additional positional arguments forwarded to the base ``_post_call``. _obp_initiated : bool, default=False Indicates whether the call originated from the OBP workflow. When ``True``, wrapped scalar metadata is normalized to use ``coverage``. \*\*kwargs : dict Additional keyword arguments forwarded to the base ``_post_call``. Returns ------- DataTreeDitto | Any The normalized ``DataTreeDitto`` output with coverage stored in ``data.ds.attrs["coverage"]``; otherwise the result returned by the base ``_post_call``. """ # Calling super()._post_call() here results in a DataTreeDitto object. The DTD # object will have the same name as the calling CoverageChecker. It will contain # a single attribute named "value" that will contain the coverage percentage. # # The code below will rename the "value" attribute to "coverage". data = super()._post_call(data, *args, _obp_initiated=_obp_initiated, **kwargs) if _obp_initiated and hasattr(data, "ds") and data.ds is not None: attrs = data.ds.attrs if "value" in attrs and "coverage" not in attrs: attrs["coverage"] = attrs.pop("value") return data
[docs]class CoverageCheckersInterface(BaseClassInterface): """Interpolation routine to apply when reprojecting data.""" name = "coverage_checkers" plugin_class = BaseCoverageCheckerPlugin required_args = {"standard": ["xarray_obj", "variable_name"]} required_kwargs = {"standard": {}} allowable_kwargs = { "standard": { "area_def", "radius_km", } }
[docs] def get_plugin_for_product(self, product, checker_field="coverage_checker"): """Get plugin for product.""" if checker_field in product: self.get_plugin(product[checker_field]["name"]) else: self.get_plugin("masked_array")
coverage_checkers = CoverageCheckersInterface()