Source code for geovista.pantry.meshes

# Copyright (c) 2021, GeoVista Contributors.
#
# This file is part of GeoVista and is distributed under the 3-Clause BSD license.
# See the LICENSE file in the package root directory for licensing details.

"""Download, cache, load and transform geovista sample data into a pyvista mesh.

Notes
-----
.. versionadded:: 0.5.0

"""

from __future__ import annotations

from typing import TYPE_CHECKING
from warnings import warn

import lazy_loader as lazy

from geovista.bridge import Transform
from geovista.cache import CACHE
from geovista.common import Preference
import geovista.pantry.data

if TYPE_CHECKING:
    import pyvista as pv

# lazy import third-party dependencies
np = lazy.load("numpy")
pooch = lazy.load("pooch")
pv = lazy.load("pyvista")

__all__ = [
    "LFRIC_RESOLUTION",
    "LFRIC_RESOLUTIONS",
    "PREFERENCE",
    "REGULAR_RESOLUTION",
    "WARP_FACTOR",
    "ZLEVEL_SCALE_CLOUD",
    "cloud_amount",
    "dynamico",
    "fesom",
    "fvcom_tamar",
    "icon_soil",
    "lam_equator",
    "lam_falklands",
    "lam_london",
    "lam_new_zealand",
    "lam_pacific",
    "lam_polar",
    "lam_uk",
    "lfric",
    "lfric_orog",
    "lfric_sst",
    "nemo_orca2",
    "nemo_orca2_cloud",
    "oisst_avhrr_sst",
    "regular_grid",
    "ww3_global_smc",
    "ww3_global_tri",
]

LFRIC_RESOLUTION: str = "c96"
"""The default LFRic model unstructured cubed-sphere resolution."""

LFRIC_RESOLUTIONS: list[str] = ["c48", "c96", "c192"]
"""The available Met Office cubed-sphere assets."""

PREFERENCE: Preference = Preference.CELL
"""The default mesh preference."""

REGULAR_RESOLUTION: str = "r60"
"""Default regular grid resolution."""

WARP_FACTOR: float = 2e-5
"""The default warp factor for mesh points."""

ZLEVEL_SCALE_CLOUD: float = 1e-5
"""Proportional multiplier for point-cloud levels/offsets."""


def _lfric_sample_to_mesh(
    sample: geovista.pantry.data.SampleUnstructuredXY,
) -> pv.PolyData:
    """Transform the provided pantry `sample` into a mesh.

    Parameters
    ----------
    sample : SampleUnstructuredXY
        The unstructured spatial coordinates and connectivity.

    Returns
    -------
    PolyData
        The unstructured cubed-sphere mesh.

    Notes
    -----
    .. versionadded:: 0.1.0

    """
    return Transform.from_unstructured(
        sample.lons,
        sample.lats,
        connectivity=sample.connectivity,
        start_index=sample.start_index,
    )


[docs] def cloud_amount( preference: str | geovista.pantry.data.CloudPreference | None = None, ) -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a Met Office c768 unstructured cubed-sphere with optional cloud amount data. Parameters ---------- preference : str or CloudPreference, optional The cloud type, which may be ``low``, ``medium``, ``high``, ``very_high`` or ``mesh``. Defaults to ``mesh``, the c768 mesh with no data payload attached. Returns ------- PolyData The unstructured cubed-sphere mesh. Notes ----- .. versionadded:: 0.4.0 """ if preference is None: preference = geovista.pantry.data.CLOUD_AMOUNT_PREFERENCE if not geovista.pantry.data.CloudPreference.valid(preference): options = " or ".join( f"{item!r}" for item in geovista.pantry.data.CloudPreference.values() ) emsg = f"Expected a preference of {options}, got '{preference}'." raise ValueError(emsg) preference = geovista.pantry.data.CloudPreference(preference) sample = geovista.pantry.data.cloud_amount(preference) mesh = _lfric_sample_to_mesh(sample) if preference != geovista.pantry.data.CloudPreference.MESH: mesh[sample.name] = sample.data return mesh
[docs] def dynamico() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a DYNAMICO icosahedral mesh. Returns ------- PolyData The DYNAMICO mesh. Notes ----- .. versionadded:: 0.3.0 """ sample = geovista.pantry.data.dynamico() return Transform.from_unstructured( sample.lons, sample.lats, data=sample.data, name=sample.name, )
[docs] def fesom() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a AWI-CM FESOM 1.4 mesh with Sea Surface Temperature data. Returns ------- PolyData The FESOM mesh. Notes ----- .. versionadded:: 0.1.0 """ sample = geovista.pantry.data.fesom() return Transform.from_unstructured( sample.lons, sample.lats, connectivity=sample.connectivity, data=sample.data, name=sample.name, )
[docs] def fvcom_tamar( preference: str | Preference | None = None, warp: bool | None = False, factor: float | None = None, ) -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a Plymouth Marine Laboratory (PML) Finite Volume Community Ocean Model (FVCOM) mesh of the Tamar Estuaries and Plymouth Sound. Parameters ---------- preference : str or Preference, optional Render the mesh using ``cell`` or ``point`` data. Defaults to :data:`PREFERENCE`. warp : bool, default=False Warp the mesh nodes by the ``point`` data. factor : float, optional The scale factor used to warp the mesh. Defaults to :data:`WARP_FACTOR`. Returns ------- PolyData The FVCOM mesh. Notes ----- .. versionadded:: 0.1.0 """ if preference is None: preference = PREFERENCE if not Preference.valid(preference): options = " or ".join(f"{item!r}" for item in Preference.values()) emsg = f"Expected a preference of {options}, got '{preference}'." raise ValueError(emsg) preference = Preference(preference) if factor is None: factor = WARP_FACTOR sample = geovista.pantry.data.fvcom_tamar() data = sample.face if preference == Preference.CELL else sample.node name = sample.name mesh = Transform.from_unstructured( sample.lons, sample.lats, connectivity=sample.connectivity, data=data, name=name, ) if warp: if preference == Preference.CELL: mesh.point_data[name] = sample.node mesh.compute_normals(cell_normals=False, point_normals=True, inplace=True) mesh.warp_by_scalar(scalars=name, inplace=True, factor=factor) mesh.set_active_scalars(name, preference.value) return mesh
[docs] def icon_soil() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate an Icosahedral Nonhydrostatic Weather and Climate Model (ICON) global 160km resolution (R02B04 grid) triangular mesh with soil type data. Returns ------- PolyData The ICON mesh. Notes ----- .. versionadded:: 0.1.0 """ sample = geovista.pantry.data.icon_soil() return Transform.from_unstructured( sample.lons, sample.lats, data=sample.data, name=sample.name, )
[docs] def lam_equator() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a C4 cubed-sphere Local Area Model (LAM) mesh located at the equator. Returns ------- PolyData The LAM mesh. Notes ----- .. versionadded:: 0.1.0 """ return _lfric_sample_to_mesh(geovista.pantry.data.lam_equator())
[docs] def lam_falklands() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a C4 cubed-sphere Local Area Model (LAM) mesh located over the Falkland Islands. Returns ------- PolyData The LAM mesh. Notes ----- .. versionadded:: 0.1.0 """ return _lfric_sample_to_mesh(geovista.pantry.data.lam_falklands())
[docs] def lam_london() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a C4 cubed-sphere Local Area Model (LAM) mesh located over London, United Kingdom. Returns ------- PolyData The LAM mesh. Notes ----- .. versionadded:: 0.1.0 """ return _lfric_sample_to_mesh(geovista.pantry.data.lam_london())
[docs] def lam_new_zealand() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a C4 cubed-sphere Local Area Model (LAM) mesh located over New Zealand. Returns ------- PolyData The LAM mesh. Notes ----- .. versionadded:: 0.1.0 """ return _lfric_sample_to_mesh(geovista.pantry.data.lam_new_zealand())
[docs] def lam_pacific() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a high-resolution Local Area Model (LAM) mesh located over the Pacific Ocean. Returns ------- PolyData The LAM mesh. Notes ----- .. versionadded:: 0.1.0 """ sample = geovista.pantry.data.lam_pacific() return Transform.from_unstructured( sample.lons, sample.lats, connectivity=sample.connectivity, data=sample.data, name=sample.name, start_index=sample.start_index, )
[docs] def lam_polar() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a C4 cubed-sphere Local Area Model (LAM) mesh located over the Polar cap. Returns ------- PolyData The LAM mesh. Notes ----- .. versionadded:: 0.1.0 """ return _lfric_sample_to_mesh(geovista.pantry.data.lam_polar())
[docs] def lam_uk() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a C4 cubed-sphere Local Area Model (LAM) mesh located over the United Kingdom. Returns ------- PolyData The LAM mesh. Notes ----- .. versionadded:: 0.1.0 """ return _lfric_sample_to_mesh(geovista.pantry.data.lam_uk())
[docs] def lfric(resolution: str | None = None) -> pv.PolyData: """Load a pre-defined LFRic mesh available from the :obj:`~geovista.cache.CACHE`. Get the LFRic model unstructured cubed-sphere at the specified `resolution`. Parameters ---------- resolution : str, optional The resolution of the LFRic model mesh, which may be either ``c48``, ``c96`` or ``c192``. Defaults to :data:`LFRIC_RESOLUTION`. Also see :data:`LFRIC_RESOLUTIONS`. Returns ------- PolyData The LFRic cubed-sphere mesh. Notes ----- .. versionadded:: 0.1.0 """ if resolution is None: resolution = LFRIC_RESOLUTION original = str(resolution) resolution = original.lower() if resolution not in LFRIC_RESOLUTIONS: wmsg = ( f"geovista detected unknown LFRic cubed-sphere resolution {original!r}, " f"using {LFRIC_RESOLUTION!r} instead." ) warn(wmsg, stacklevel=2) resolution = LFRIC_RESOLUTION fname = f"lfric_{resolution}.vtk" processor = pooch.Decompress(method="auto", name=fname) resource = CACHE.fetch(f"pantry/meshes/{fname}.bz2", processor=processor) return pv.read(resource)
[docs] def lfric_orog(warp: bool | None = False, factor: float | None = None) -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a global surface altitude mesh. Parameters ---------- warp : bool, default=False Warp the mesh nodes by the orography ``point`` data. factor : float, optional The scale factor used to warp the mesh. Defaults to :data:`WARP_FACTOR`. Returns ------- PolyData The orography mesh. Notes ----- .. versionadded:: 0.1.0 """ if factor is None: factor = WARP_FACTOR sample = geovista.pantry.data.lfric_orog() name = sample.name mesh = Transform.from_unstructured( sample.lons, sample.lats, connectivity=sample.connectivity, data=sample.data, name=name, start_index=sample.start_index, ) if warp: mesh.compute_normals(cell_normals=False, point_normals=True, inplace=True) mesh.warp_by_scalar(scalars=name, inplace=True, factor=factor) return mesh
[docs] def lfric_sst() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a global Sea Surface Temperature (SST) mesh. Returns ------- PolyData The SST mesh. Notes ----- .. versionadded:: 0.1.0 """ sample = geovista.pantry.data.lfric_sst() return Transform.from_unstructured( sample.lons, sample.lats, connectivity=sample.connectivity, data=sample.data, name=sample.name, start_index=sample.start_index, )
[docs] def nemo_orca2() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a global Sea Water Potential Temperature ORCA2 mesh. Returns ------- PolyData The ORCA2 mesh. Notes ----- .. versionadded:: 0.1.0 """ sample = geovista.pantry.data.nemo_orca2() return Transform.from_2d( sample.lons, sample.lats, data=sample.data, name=sample.name )
[docs] def nemo_orca2_cloud(zscale: float | None = None) -> pv.PolyData: """Create a point-cloud mesh from :mod:`geovista.pantry.data` sample data. Generate an ORCA2 point-cloud of Sea Water Potential Temperature gradients. Parameters ---------- zscale : float, optional The proportional multiplier for z-axis ``zlevel``. Defaults to :data:`ZLEVEL_SCALE_CLOUD`. Returns ------- PolyData The ORCA2 point-cloud. Notes ----- .. versionadded:: 0.2.0 """ sample = geovista.pantry.data.nemo_orca2_gradient() zscale = ZLEVEL_SCALE_CLOUD if zscale is None else float(zscale) return Transform.from_points( sample.lons, sample.lats, data=sample.zlevel, name=sample.name, zlevel=-sample.zlevel, zscale=zscale, )
[docs] def oisst_avhrr_sst() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a global Sea Surface Temperature (SST) NOAA/NCEI OISST AVHRR mesh. Returns ------- PolyData The SST mesh. Notes ----- .. versionadded:: 0.1.0 """ sample = geovista.pantry.data.oisst_avhrr_sst() return Transform.from_1d( sample.lons, sample.lats, data=sample.data, name=sample.name, )
[docs] def regular_grid( resolution: str | None = None, radius: float | None = None, ) -> pv.PolyData: """Generate a regular grid given the `resolution`. Parameters ---------- resolution : str, optional In the format of ``rN``, where ``N`` is the number of cells in latitude, and ``N * 1.5`` cells in longitude. Defaults to :data:`REGULAR_RESOLUTION`. radius : float, optional The radius of the sphere. Defaults to :data:`geovista.common.RADIUS`. Returns ------- PolyData The regular longitude/latitude grid mesh. Notes ----- .. versionadded:: 0.3.0 """ if resolution is None: resolution = REGULAR_RESOLUTION original = str(resolution) resolution = original.lower() def warn_unknown() -> None: """Generate warning message for invalid resolution.""" wmsg = ( f"Unknown regular grid resolution {original!r}, using " f"{REGULAR_RESOLUTION!r} instead." ) warn(wmsg, stacklevel=2) if resolution.startswith("r"): try: n_cells = int(resolution.split("r")[1]) except ValueError: warn_unknown() n_cells = int(REGULAR_RESOLUTION.split("r")[1]) else: warn_unknown() n_cells = int(REGULAR_RESOLUTION.split("r")[1]) lats = np.linspace(-90.0, 90.0, n_cells + 1) lons = np.linspace(-180.0, 180.0, int(n_cells * 1.5) + 1) return Transform.from_1d(lons, lats, radius=radius)
[docs] def ww3_global_smc(step: int | None = None) -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a global Sea Surface Wave Significant Height WAVEWATCH III (WW3) Spherical Multi-Cell (SMC) mesh. Parameters ---------- step : int, default=0 The time-series offset. Returns ------- PolyData The WW3 SMC mesh. Notes ----- .. versionadded:: 0.1.0 """ sample = geovista.pantry.data.ww3_global_smc(step=step) return Transform.from_unstructured( sample.lons, sample.lats, connectivity=sample.connectivity, data=sample.data, name=sample.name, )
[docs] def ww3_global_tri() -> pv.PolyData: """Create a mesh from :mod:`geovista.pantry.data` sample data. Generate a global Sea Surface Wave Significant Height WAVEWATCH III (WW3) triangular mesh. Returns ------- PolyData The WW3 mesh. Notes ----- .. versionadded:: 0.1.0 """ sample = geovista.pantry.data.ww3_global_tri() return Transform.from_unstructured( sample.lons, sample.lats, connectivity=sample.connectivity, data=sample.data, name=sample.name, )