From 65c4189aa4cd571b3bd666b340618aef6003f84c Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Fri, 25 Oct 2024 17:32:07 +0200 Subject: [PATCH] API documentation (#81) * configure the api documentation * document grid info objects * expose the dggs info classes * generate entries for the grid info objects * add `xarray`, `numpy` and `pandas` intersphinx entries * install the project * expose the plotting mechanism * get the documentation to pass without warnings * minimal docstring for `HealpixInfo.nside` * minimal docstring for `HealpixInfo.nest` * remove `Dataset.dggs.explore`, which errors currently * docstrings for `cell_centers` and `cell_boundaries` * document `*.dggs.grid_info` * docstring for `from_dict` * remove the trailing slash from the toctree dir * document `tutorial.open_dataset` * clarify the role of the grid names * set some generic aliases * improve the docstrings of `index` and `coord` * make `cell_ids` an alias of `coord` * restructure the api docs * document the healpix grid info object * document the h3 grid info class * don't document the `valid_parameters` class attribute * don't count `unique` as a valid indexing scheme * document `xdggs.decode` * link to the documentation in the package metadata --- .gitignore | 1 + ci/docs.yml | 1 + docs/api-hidden.rst | 35 ++++++++++++++ docs/api.rst | 92 ++++++++++++++++++++++++++++++++++++ docs/conf.py | 45 +++++++++++++++++- docs/index.md | 4 ++ pyproject.toml | 2 +- xdggs/__init__.py | 16 +++++-- xdggs/accessor.py | 47 ++++++++++++++++--- xdggs/h3.py | 83 ++++++++++++++++++++++++++++++++ xdggs/healpix.py | 94 +++++++++++++++++++++++++++++++++++++ xdggs/index.py | 15 ++++++ xdggs/tests/test_healpix.py | 7 ++- xdggs/tutorial.py | 4 +- 14 files changed, 428 insertions(+), 18 deletions(-) create mode 100644 docs/api-hidden.rst create mode 100644 docs/api.rst diff --git a/.gitignore b/.gitignore index ee5aa2d7..e13a0960 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/generated/ # PyBuilder .pybuilder/ diff --git a/ci/docs.yml b/ci/docs.yml index bd1c5f64..81d43ccf 100644 --- a/ci/docs.yml +++ b/ci/docs.yml @@ -27,3 +27,4 @@ dependencies: - pip - pip: - h3ronpy + - -e .. diff --git a/docs/api-hidden.rst b/docs/api-hidden.rst new file mode 100644 index 00000000..0dc483b0 --- /dev/null +++ b/docs/api-hidden.rst @@ -0,0 +1,35 @@ +:orphan: + +.. currentmodule:: xdggs + +.. autosummary:: + :toctree: generated + + DGGSInfo.resolution + + DGGSInfo.from_dict + DGGSInfo.to_dict + DGGSInfo.cell_boundaries + DGGSInfo.cell_ids2geographic + DGGSInfo.geographic2cell_ids + + HealpixInfo.resolution + HealpixInfo.indexing_scheme + HealpixInfo.valid_parameters + HealpixInfo.nside + HealpixInfo.nest + + HealpixInfo.from_dict + HealpixInfo.to_dict + HealpixInfo.cell_boundaries + HealpixInfo.cell_ids2geographic + HealpixInfo.geographic2cell_ids + + H3Info.resolution + H3Info.valid_parameters + + H3Info.from_dict + H3Info.to_dict + H3Info.cell_boundaries + H3Info.cell_ids2geographic + H3Info.geographic2cell_ids diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..0fea7c31 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,92 @@ +.. _api: + +############# +API reference +############# + +Top-level functions +=================== + +.. currentmodule:: xdggs + +.. autosummary:: + :toctree: generated + + decode + +Grid parameter objects +====================== + +.. autosummary:: + :toctree: generated + + DGGSInfo + + HealpixInfo + H3Info + +.. currentmodule:: xarray + +Dataset +======= + +Parameters +---------- +.. autosummary:: + :toctree: generated + :template: autosummary/accessor_attribute.rst + + Dataset.dggs.grid_info + Dataset.dggs.params + + +Data inference +-------------- + +.. autosummary:: + :toctree: generated + :template: autosummary/accessor_method.rst + + Dataset.dggs.cell_centers + Dataset.dggs.cell_boundaries + +DataArray +========= + +Parameters +---------- +.. autosummary:: + :toctree: generated + :template: autosummary/accessor_attribute.rst + + DataArray.dggs.grid_info + DataArray.dggs.params + + +Data inference +-------------- + +.. autosummary:: + :toctree: generated + :template: autosummary/accessor_method.rst + + DataArray.dggs.cell_centers + DataArray.dggs.cell_boundaries + +Plotting +-------- +.. autosummary:: + :toctree: generated + :template: autosummary/accessor_method.rst + + DataArray.dggs.explore + +Tutorial +======== + +.. currentmodule:: xdggs + +.. autosummary:: + :toctree: generated + + tutorial.open_dataset diff --git a/docs/conf.py b/docs/conf.py index 06c27020..7a378f35 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,10 @@ # -- Project information ----------------------------------------------------- import datetime as dt +import sphinx_autosummary_accessors + +import xdggs # noqa: F401 + project = "xdggs" author = f"{project} developers" initial_year = "2023" @@ -18,9 +22,13 @@ extensions = [ "sphinx.ext.extlinks", "sphinx.ext.intersphinx", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", "IPython.sphinxext.ipython_directive", "IPython.sphinxext.ipython_console_highlighting", "myst_parser", + "sphinx_autosummary_accessors", ] extlinks = { @@ -29,13 +37,42 @@ } # Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] +templates_path = ["_templates", sphinx_autosummary_accessors.templates_path] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "directory"] +# -- autosummary / autodoc --------------------------------------------------- + +autosummary_generate = True +autodoc_typehints = "none" + +# -- napoleon ---------------------------------------------------------------- + +napoleon_numpy_docstring = True +napoleon_use_param = False +napoleon_use_rtype = False +napoleon_preprocess_types = True +napoleon_type_aliases = { + # general terms + "sequence": ":term:`sequence`", + "iterable": ":term:`iterable`", + "callable": ":py:func:`callable`", + "dict_like": ":term:`dict-like `", + "dict-like": ":term:`dict-like `", + "path-like": ":term:`path-like `", + "mapping": ":term:`mapping`", + "file-like": ":term:`file-like `", + "any": ":py:class:`any `", + # numpy terms + "array_like": ":term:`array_like`", + "array-like": ":term:`array-like `", + "scalar": ":term:`scalar`", + "array": ":term:`array`", + "hashable": ":term:`hashable `", +} # -- Options for HTML output ------------------------------------------------- @@ -54,4 +91,10 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "sphinx": ("https://www.sphinx-doc.org/en/stable/", None), + "numpy": ("https://numpy.org/doc/stable", None), + "xarray": ("https://docs.xarray.dev/en/latest/", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), + "lonboard": ("https://developmentseed.org/lonboard/latest", None), + "healpy": ("https://healpy.readthedocs.io/en/latest", None), + "shapely": ("https://shapely.readthedocs.io/en/stable", None), } diff --git a/docs/index.md b/docs/index.md index e456944b..964c2502 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1,5 @@ # welcome to the documentation of `xdggs` + +```{toctree} +api.rst +``` diff --git a/pyproject.toml b/pyproject.toml index ee788a76..c5ca828b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ ] [project.urls] -# Home = "https://xdggs.readthedocs.io" +Documentation = "https://xdggs.readthedocs.io" Repository = "https://github.com/xarray-contrib/xdggs" [tool.ruff] diff --git a/xdggs/__init__.py b/xdggs/__init__.py index f47320b8..41690ec9 100644 --- a/xdggs/__init__.py +++ b/xdggs/__init__.py @@ -2,8 +2,9 @@ import xdggs.tutorial # noqa: F401 from xdggs.accessor import DGGSAccessor # noqa: F401 -from xdggs.h3 import H3Index -from xdggs.healpix import HealpixIndex +from xdggs.grid import DGGSInfo +from xdggs.h3 import H3Index, H3Info +from xdggs.healpix import HealpixIndex, HealpixInfo from xdggs.index import DGGSIndex, decode try: @@ -12,4 +13,13 @@ # package is not installed __version__ = "9999" -__all__ = ["__version__", "DGGSIndex", "H3Index", "HealpixIndex", "decode"] +__all__ = [ + "__version__", + "DGGSInfo", + "H3Info", + "HealpixInfo", + "DGGSIndex", + "H3Index", + "HealpixIndex", + "decode", +] diff --git a/xdggs/accessor.py b/xdggs/accessor.py index df6e1860..72792a14 100644 --- a/xdggs/accessor.py +++ b/xdggs/accessor.py @@ -31,9 +31,12 @@ def __init__(self, obj: xr.Dataset | xr.DataArray): @property def index(self) -> DGGSIndex: - """Returns the DGGSIndex instance for this Dataset or DataArray. + """The DGGSIndex instance for this Dataset or DataArray. - Raise a ``ValueError`` if no such index is found. + Raises + ------ + ValueError + if no DGGSIndex can be found """ if self._index is None: raise ValueError("no DGGSIndex found on this Dataset or DataArray") @@ -41,10 +44,12 @@ def index(self) -> DGGSIndex: @property def coord(self) -> xr.DataArray: - """Returns the indexed DGGS (cell ids) coordinate as a DataArray. - - Raise a ``ValueError`` if no such coordinate is found on this Dataset or DataArray. + """The indexed DGGS (cell ids) coordinate as a DataArray. + Raises + ------ + ValueError + if no such coordinate is found on the Dataset / DataArray """ if not self._name: raise ValueError( @@ -59,6 +64,12 @@ def params(self) -> dict: @property def grid_info(self) -> DGGSInfo: + """The grid info object containing the DGGS type and its parameters. + + Returns + ------- + xdggs.DGGSInfo + """ return self.index.grid_info def sel_latlon( @@ -78,7 +89,6 @@ def sel_latlon( subset A new :py:class:`xarray.Dataset` or :py:class:`xarray.DataArray` with all cells that contain the input latitude/longitude data points. - """ cell_indexers = { self._name: self.grid_info.geographic2cell_ids(latitude, longitude) @@ -98,9 +108,25 @@ def assign_latlon_coords(self) -> xr.Dataset | xr.DataArray: @property def cell_ids(self): - return self._obj[self._name] + """The indexed DGGS (cell ids) coordinate as a DataArray. + + Alias of ``coord``. + + Raises + ------ + ValueError + if no such coordinate is found on the Dataset / DataArray + """ + return self.coord def cell_centers(self): + """derive geographic cell center coordinates + + Returns + ------- + coords : xarray.Dataset + Dataset containing the cell centers in geographic coordinates. + """ lon_data, lat_data = self.index.cell_centers() return xr.Dataset( @@ -111,6 +137,13 @@ def cell_centers(self): ) def cell_boundaries(self): + """derive cell boundary polygons + + Returns + ------- + boundaries : xarray.DataArray + The cell boundaries as shapely objects. + """ boundaries = self.index.cell_boundaries() return xr.DataArray( diff --git a/xdggs/h3.py b/xdggs/h3.py index 40bf2896..a38592b8 100644 --- a/xdggs/h3.py +++ b/xdggs/h3.py @@ -60,7 +60,17 @@ def polygons_geoarrow(wkb): @dataclass(frozen=True) class H3Info(DGGSInfo): + """ + Grid information container for h3 grids. + + Parameters + ---------- + resolution : int + The resolution of the grid + """ + resolution: int + """int : The resolution of the grid""" valid_parameters: ClassVar[dict[str, Any]] = {"resolution": range(16)} @@ -70,23 +80,96 @@ def __post_init__(self): @classmethod def from_dict(cls: type[Self], mapping: dict[str, Any]) -> Self: + """construct a `H3Info` object from a mapping of attributes + + Parameters + ---------- + mapping: mapping of str to any + The attributes. + + Returns + ------- + grid_info : H3Info + The constructed grid info object. + """ + params = {k: v for k, v in mapping.items() if k != "grid_name"} return cls(**params) def to_dict(self: Self) -> dict[str, Any]: + """ + Dump the normalized grid parameters. + + Returns + ------- + mapping : dict of str to any + The normalized grid parameters. + """ return {"grid_name": "h3", "resolution": self.resolution} def cell_ids2geographic( self, cell_ids: np.ndarray ) -> tuple[np.ndarray, np.ndarray]: + """ + Convert cell ids to geographic coordinates + + Parameters + ---------- + cell_ids : array-like + Array-like containing the cell ids. + + Returns + ------- + lon : array-like + The longitude coordinate values of the grid cells in degree + lat : array-like + The latitude coordinate values of the grid cells in degree + """ lat, lon = cells_to_coordinates(cell_ids, radians=False) return lon, lat def geographic2cell_ids(self, lon, lat): + """ + Convert cell ids to geographic coordinates + + This will perform a binning operation: any point within a grid cell will be assign + that cell's ID. + + Parameters + ---------- + lon : array-like + The longitude coordinate values in degree + lat : array-like + The latitude coordinate values in degree + + Returns + ------- + cell_ids : array-like + Array-like containing the cell ids. + """ return coordinates_to_cells(lat, lon, self.resolution, radians=False) def cell_boundaries(self, cell_ids, backend="shapely"): + """ + Derive cell boundary polygons from cell ids + + Parameters + ---------- + cell_ids : array-like + The cell ids. + backend : {"shapely", "geoarrow"}, default: "shapely" + The backend to convert to. + + Returns + ------- + polygons : array-like + The derived cell boundary polygons. The format differs based on the passed + backend: + + - ``"shapely"``: return a array of :py:class:`shapely.Polygon` objects + - ``"geoarrow"``: return a ``geoarrow`` array + """ # TODO: convert cell ids directly to geoarrow once h3ronpy supports it wkb = cells_to_wkb_polygons(cell_ids, radians=False, link_cells=False) diff --git a/xdggs/healpix.py b/xdggs/healpix.py index db47955c..6db12dbe 100644 --- a/xdggs/healpix.py +++ b/xdggs/healpix.py @@ -95,9 +95,26 @@ def center_around_prime_meridian(lon, lat): @dataclass(frozen=True) class HealpixInfo(DGGSInfo): + """ + Grid information container for healpix grids. + + Parameters + ---------- + resolution : int + The resolution of the grid + indexing_scheme : {"nested", "ring", "unique"}, default: "nested" + The indexing scheme of the healpix grid. + + .. warning:: + Note that ``"unique"`` is currently not supported as the underlying library + (:doc:`healpy `) does not support it. + """ + resolution: int + """int : The resolution of the grid""" indexing_scheme: Literal["nested", "ring", "unique"] = "nested" + """int : The indexing scheme of the grid""" valid_parameters: ClassVar[dict[str, Any]] = { "resolution": range(0, 29 + 1), @@ -112,13 +129,17 @@ def __post_init__(self): raise ValueError( f"indexing scheme must be one of {self.valid_parameters['indexing_scheme']}" ) + elif self.indexing_scheme == "unique": + raise ValueError("the indexing scheme `unique` is currently not supported") @property def nside(self: Self) -> int: + """resolution as the healpy-compatible nside parameter""" return 2**self.resolution @property def nest(self: Self) -> bool: + """indexing_scheme as the healpy-compatible nest parameter""" if self.indexing_scheme not in {"nested", "ring"}: raise ValueError( f"cannot convert indexing scheme {self.indexing_scheme} to `nest`" @@ -128,6 +149,19 @@ def nest(self: Self) -> bool: @classmethod def from_dict(cls: type[T], mapping: dict[str, Any]) -> T: + """construct a `HealpixInfo` object from a mapping of attributes + + Parameters + ---------- + mapping: mapping of str to any + The attributes. + + Returns + ------- + grid_info : HealpixInfo + The constructed grid info object. + """ + def translate_nside(nside): log = np.log2(nside) potential_resolution = int(log) @@ -176,6 +210,14 @@ def translate(name, value): return cls(**params) def to_dict(self: Self) -> dict[str, Any]: + """ + Dump the normalized grid parameters. + + Returns + ------- + mapping : dict of str to any + The normalized grid parameters. + """ return { "grid_name": "healpix", "resolution": self.resolution, @@ -183,14 +225,66 @@ def to_dict(self: Self) -> dict[str, Any]: } def cell_ids2geographic(self, cell_ids): + """ + Convert cell ids to geographic coordinates + + Parameters + ---------- + cell_ids : array-like + Array-like containing the cell ids. + + Returns + ------- + lon : array-like + The longitude coordinate values of the grid cells in degree + lat : array-like + The latitude coordinate values of the grid cells in degree + """ lon, lat = healpy.pix2ang(self.nside, cell_ids, nest=self.nest, lonlat=True) return lon, lat def geographic2cell_ids(self, lon, lat): + """ + Convert cell ids to geographic coordinates + + This will perform a binning operation: any point within a grid cell will be assign + that cell's ID. + + Parameters + ---------- + lon : array-like + The longitude coordinate values in degree + lat : array-like + The latitude coordinate values in degree + + Returns + ------- + cell_ids : array-like + Array-like containing the cell ids. + """ return healpy.ang2pix(self.nside, lon, lat, lonlat=True, nest=self.nest) def cell_boundaries(self, cell_ids: Any, backend="shapely") -> np.ndarray: + """ + Derive cell boundary polygons from cell ids + + Parameters + ---------- + cell_ids : array-like + The cell ids. + backend : {"shapely", "geoarrow"}, default: "shapely" + The backend to convert to. + + Returns + ------- + polygons : array-like + The derived cell boundary polygons. The format differs based on the passed + backend: + + - ``"shapely"``: return a array of :py:class:`shapely.Polygon` objects + - ``"geoarrow"``: return a ``geoarrow`` array + """ boundary_vectors = healpy.boundaries( self.nside, cell_ids, step=1, nest=self.nest ) diff --git a/xdggs/index.py b/xdggs/index.py index 36eb6138..13149d9d 100644 --- a/xdggs/index.py +++ b/xdggs/index.py @@ -10,6 +10,21 @@ def decode(ds): + """ + decode grid parameters and create a DGGS index + + Parameters + ---------- + ds : xarray.Dataset + The input dataset. Must contain a `"cell_ids"` coordinate with at least + the attributes `grid_name` and `resolution`. + + Returns + ------- + decoded : xarray.Dataset + The input dataset with a DGGS index on the ``"cell_ids"`` coordinate. + """ + variable_name = "cell_ids" return ds.drop_indexes(variable_name, errors="ignore").set_xindex( diff --git a/xdggs/tests/test_healpix.py b/xdggs/tests/test_healpix.py index d823f36f..5de2f7f5 100644 --- a/xdggs/tests/test_healpix.py +++ b/xdggs/tests/test_healpix.py @@ -24,10 +24,9 @@ class strategies: invalid_resolutions = st.integers(max_value=-1) | st.integers(min_value=30) resolutions = st.integers(min_value=0, max_value=29) - indexing_schemes = st.sampled_from(["nested", "ring", "unique"]) - invalid_indexing_schemes = st.text().filter( - lambda x: x not in ["nested", "ring", "unique"] - ) + # TODO: add back `"unique"` once that is supported + indexing_schemes = st.sampled_from(["nested", "ring"]) + invalid_indexing_schemes = st.text().filter(lambda x: x not in ["nested", "ring"]) dims = xrst.names() diff --git a/xdggs/tutorial.py b/xdggs/tutorial.py index ad6fffd6..7405f642 100644 --- a/xdggs/tutorial.py +++ b/xdggs/tutorial.py @@ -69,9 +69,9 @@ def open_dataset( If a local copy is found then always use that to avoid network traffic. - Available datasets: + Available datasets (available grid names in parentheses): - * ``"air_temperature"`` (H3, healpix): NCEP reanalysis subset. + * ``"air_temperature"`` (``h3``, ``healpix``): NCEP reanalysis subset. Parameters ----------