geovista.bridge のソースコード
# 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.
"""Transform structured grids and unstructured meshes to PyVista format.
This module provides the :class:`~geovista.bridge.Transform` factory class for
transforming rectilinear, curvilinear, and unstructured geospatial data
into geolocated :doc:`PyVista <pyvista:index>` mesh instances.
Notes
-----
.. versionadded:: 0.1.0
- :doc:`pyvista:user-guide/what-is-a-mesh`
"""
from __future__ import annotations
from collections.abc import Iterable
import pathlib
from typing import TYPE_CHECKING
import warnings
import lazy_loader as lazy
from .common import (
GV_FIELD_NAME,
GV_FIELD_RADIUS,
GV_FIELD_ZSCALE,
RADIUS,
ZLEVEL_SCALE,
VectorLike,
cast_UnstructuredGrid_to_PolyData,
nan_mask,
to_cartesian,
vectors_to_cartesian,
)
from .crs import WGS84, CRSLike, to_wkt
from .transform import transform_points
if TYPE_CHECKING:
import numpy as np
from numpy.typing import ArrayLike
import pyvista as pv
# lazy import third-party dependencies
np = lazy.load("numpy")
pv = lazy.load("pyvista")
pyproj = lazy.load("pyproj")
rio = lazy.load("rasterio")
__all__ = [
"BRIDGE_CLEAN",
"NAME_CELLS",
"NAME_POINTS",
"NAME_VECTORS",
"RIO_SIEVE_SIZE",
"PathLike",
"Shape",
"Transform",
]
type PathLike = str | pathlib.Path
"""Type alias for an asset file path."""
type Shape = tuple[int, ...]
"""Type alias for a tuple of integers."""
# constants
BRIDGE_CLEAN: bool = False
"""Whether mesh cleaning performed by the bridge."""
NAME_CELLS: str = "cell_data"
"""Default array name for data on the mesh cells."""
NAME_POINTS: str = "point_data"
"""Default array name for data on the mesh points."""
NAME_VECTORS: str = "vector_data"
"""Default array name for mesh vectors."""
RIO_SIEVE_SIZE: int = 800
"""The default size of the :func:`rasterio.features.sieve` filter."""
[ドキュメント]
class Transform: # numpydoc ignore=PR01
"""Build a mesh from spatial points, connectivity, data and CRS metadata.
Notes
-----
.. versionadded:: 0.1.0
"""
@staticmethod
def _as_compatible_data(
data: ArrayLike,
n_points: int,
n_cells: int,
/,
*,
rgb: bool = False,
) -> np.ndarray:
"""Ensure data is compatible with the number of mesh points or cells.
Note that masked values will be filled with NaNs.
Parameters
----------
data : ArrayLike
The intended data payload of the mesh.
n_points : int
The number of nodes in the mesh.
n_cells : int
The number of cells in the mesh.
rgb : bool, default=False
Whether the data is an ``RGB`` or ``RGBA`` image.
Returns
-------
ndarray
The verified data.
Notes
-----
.. versionadded:: 0.1.0
"""
if data is not None:
data = np.asanyarray(data)
if rgb and data.ndim < 2:
emsg = f"RGB/RGBA image data must be at least 2D, got {data.ndim}D."
raise ValueError(emsg)
size = data.size // data.shape[-1] if rgb else data.size
if size not in (n_points, n_cells):
emsg = (
f"Require mesh data with either '{n_points:,d}' points or "
f"'{n_cells:,d}' cells, got '{data.size:,d}' values."
)
raise ValueError(emsg)
data = nan_mask(np.ravel(data))
if rgb:
# reshape to be (N, 3) or (N, 4) for RGB or RGBA image data
data = data.reshape(size, -1)
return data
@staticmethod
def _as_contiguous_1d(
xs: ArrayLike, ys: ArrayLike
) -> tuple[np.ndarray, np.ndarray]:
"""Construct contiguous 1D x-axis and y-axis bounds arrays.
Verify and return a contiguous (N+1,) x-axis and (M+1,) y-axis
bounds array, that will be then used afterwards to build a (M, N)
contiguous quad-mesh consisting of M*N faces.
Parameters
----------
xs : ArrayLike
A (N+1,) or (N, 2) x-axis array i.e., N-faces in the x-axis.
ys : ArrayLike
A (M+1,) or (M, 2) y-axis array i.e., M-faces in the y-axis.
Returns
-------
tuple of ndarray
The x-values and y-values as contiguous 1D bounds arrays.
Notes
-----
.. versionadded:: 0.1.0
"""
xs, ys = np.asanyarray(xs), np.asanyarray(ys)
if xs.ndim not in (1, 2) or (xs.ndim == 2 and xs.shape[1] != 2):
emsg = (
"Require a 1D '(N+1,)' x-axis array, or 2D '(N, 2)' "
f"x-axis array, got {xs.ndim}D '{xs.shape}'."
)
raise ValueError(emsg)
if ys.ndim not in (1, 2) or (ys.ndim == 2 and ys.shape[1] != 2):
emsg = (
"Require a 1D '(M+1,)' y-axis array, or 2D '(M, 2)' "
f"y-axis array, got {ys.ndim}D '{ys.shape}'."
)
raise ValueError(emsg)
if xs.ndim == 1 and xs.size < 2:
emsg = (
"Require a 1D x-axis array with minimal shape '(2,)' i.e., "
"one face with two x-value bounds."
)
raise ValueError(emsg)
if ys.ndim == 1 and ys.size < 2:
emsg = (
"Require a 1D y-axis array with minimal shape '(2,)' i.e., "
"one face with two y-value bounds."
)
raise ValueError(emsg)
def _contiguous(bnds: ArrayLike, kind: str) -> np.ndarray:
"""Verify and construct a contiguous bounds array.
Parameters
----------
bnds : ArrayLike
The bounds array.
kind : str
The kind of bounds array e.g., 'x-axis' or 'y-axis'.
Returns
-------
ndarray
The contiguous bounds array.
"""
left, right = bnds[:-1, 1], bnds[1:, 0]
if not np.allclose(left, right):
emsg = (
f"The {kind} bounds array, shape '{bnds.shape}', is not contiguous."
)
raise ValueError(emsg)
parts = ([bnds[0, 0]], left, [bnds[-1, -1]])
return np.concatenate(parts)
if xs.ndim == 2:
xs = _contiguous(xs, "x-axis") if xs.size > 2 else xs[0]
if ys.ndim == 2:
ys = _contiguous(ys, "y-axis") if ys.size > 2 else ys[0]
return xs, ys
@staticmethod
def _create_connectivity_m1n1(shape: Shape) -> np.ndarray:
"""Create 2D quad-mesh connectivity from node `shape`.
The connectivity ordering of the quad-mesh face nodes (points) is
anti-clockwise, as follows:
3---2
| |
0---1
Assumes that the associated face node spatial coordinates are
appropriately ordered.
Parameters
----------
shape : Shape
The shape of the 2D mesh nodes.
Returns
-------
ndarray
The connectivity array of the face-to-node offsets (zero based).
Notes
-----
..versionadded:: 0.1.0
For VTK Quadrilateral cell type node ordering see
https://examples.vtk.org/site/VTKBook/05Chapter5/#54-cell-types.
"""
# sanity check - internally this should always be the case
assert len(shape) == 2
npts = np.prod(shape)
idxs = np.arange(npts, dtype=np.uint32).reshape(shape)
nodes_c0 = np.ravel(idxs[1:, :-1]).reshape(-1, 1)
nodes_c1 = np.ravel(idxs[1:, 1:]).reshape(-1, 1)
nodes_c2 = np.ravel(idxs[:-1, 1:]).reshape(-1, 1)
nodes_c3 = np.ravel(idxs[:-1, :-1]).reshape(-1, 1)
return np.hstack([nodes_c0, nodes_c1, nodes_c2, nodes_c3])
@staticmethod
def _create_connectivity_mn4(shape: Shape) -> np.ndarray:
"""Create 2D quad-mesh connectivity from face `shape`.
The connectivity ordering of the quad-mesh face nodes (points) is
anti-clockwise, as follows:
3---2
| |
0---1
Assumes that the associated face node spatial coordinates are
appropriately ordered.
Parameters
----------
shape : Shape
The shape of the 2D mesh faces.
Returns
-------
ndarray
The connectivity array of face-to-node offsets (zero-based).
Notes
-----
..versionadded:: 0.1.0
For VTK Quadrilateral cell type node ordering see
https://examples.vtk.org/site/VTKBook/05Chapter5/#54-cell-types.
"""
# sanity check - internally this should always be the case
assert len(shape) == 2
# we know that we can only be dealing with a quad mesh
npts = np.prod(shape) * 4
return np.arange(npts, dtype=np.uint32).reshape(-1, 4)
@staticmethod
def _verify_2d(xs: ArrayLike, ys: ArrayLike) -> None:
"""Ensure compatible quad-mesh dimensionality and shape.
Verify the fitness of the provided x-values and y-values to create
a (M, N) quad-mesh consisting of M*N faces.
Parameters
----------
xs : ArrayLike
A (M+1, N+1) or (M, N, 4) x-axis array.
ys : ArrayLike
A (M+1, N+1) or (M, N, 4) y-axis array.
Notes
-----
.. versionadded:: 0.1.0
"""
if xs.shape != ys.shape:
emsg = (
"Require x-values and y-values with the same shape, got "
f"'{xs.shape}' and '{ys.shape}' respectively."
)
raise ValueError(emsg)
if xs.ndim not in (2, 3) or (xs.ndim == 3 and xs.shape[2] != 4):
emsg = (
"Require 2D x-values with shape '(M+1, N+1)', or 3D "
f"x-values with shape '(M, N, 4)', got {xs.ndim}D with "
f"shape '{xs.shape}'."
)
raise ValueError(emsg)
if xs.ndim == 2 and (xs.shape[0] < 2 or xs.shape[1] < 2):
emsg = (
"Require a quad-mesh with at least one face and four "
"points/vertices i.e., minimal shape '(2, 2)', got "
f"x-values/y-values with shape '{xs.shape}'."
)
raise ValueError(emsg)
@staticmethod
def _verify_connectivity(connectivity: Shape) -> None:
"""Ensure compatible 2D connectivity.
The connectivity shape must be 2D and contain at least the minimal number of
indices to construct a mesh with a single triangular face.
Parameters
----------
connectivity : Shape
The 2D shape of the connectivity, which must be at least (1, 3)
for the minimal mesh consisting of one triangular face.
Notes
-----
.. versionadded:: 0.1.0
"""
if len(connectivity) != 2:
emsg = (
"Require a 2D '(M, N)' connectivity array, defining the "
"N indices for each of the M-faces of the mesh, got "
f"{len(connectivity)}D connectivity array with shape "
f"'{connectivity}'."
)
raise ValueError(emsg)
if connectivity[1] < 3:
emsg = (
"Require a connectivity array defining at least 3 vertices "
"per mesh face (triangles) i.e., minimal shape '(M, 3)', got "
f"connectivity with shape '{connectivity}'."
)
raise ValueError(emsg)
[ドキュメント]
@classmethod
def from_1d(
cls,
xs: ArrayLike,
ys: ArrayLike,
/,
*,
data: ArrayLike | None = None,
name: str | None = None,
crs: CRSLike | None = None,
rgb: bool | None = False,
radius: float | None = None,
zlevel: int | None = None,
zscale: float | None = None,
clean: bool | None = None,
) -> pv.PolyData:
"""Build a quad-faced mesh from contiguous 1D x-values and y-values.
This allows the construction of a uniform or rectilinear quad-faced
``(M, N)`` mesh grid, where the mesh has ``M``-faces in the y-axis, and
``N``-faces in the x-axis, resulting in a mesh consisting of ``M*N`` faces.
The provided `xs` and `ys` will be projected from their `crs` to
geographic longitude and latitude values.
Parameters
----------
xs : ArrayLike
A 1D array of x-values, in canonical `crs` units, defining the
contiguous face x-value boundaries of the mesh. Creating a mesh
with ``N``-faces in the `crs` x-axis requires a ``(N+1,)`` array.
Alternatively, a ``(N, 2)`` contiguous bounds array may be provided.
ys : ArrayLike
A 1D array of y-values, in canonical `crs` units, defining the
contiguous face y-value boundaries of the mesh. Creating a mesh
with ``M``-faces in the `crs` y-axis requires a ``(M+1,)`` array.
Alternatively, a ``(M, 2)`` contiguous bounds array may be provided.
data : ArrayLike, optional
Data to be optionally attached to the mesh. The size of the data
must match either the shape of the fully formed mesh points
``(M+1)*(N+1)``,
or the number of mesh faces, ``M*N`` (but see the `rgb` parameter).
name : str, optional
The name of the optional data array to be attached to the mesh. If
`data` is provided but with no `name`, defaults to either
:data:`NAME_POINTS` or :data:`NAME_CELLS`.
crs : CRSLike, optional
The Coordinate Reference System of the provided `xs` and `ys`. May
be anything accepted by :meth:`pyproj.crs.CRS.from_user_input`. Defaults
to ``EPSG:4326`` i.e., ``WGS 84``.
rgb : bool, default=False
Whether `data` is an ``RGB`` or ``RGBA`` image. When ``rgb=True``,
`data` is expected to have an extra dimension for the colour
channels (length ``3`` or ``4``).
radius : float, optional
The radius of the sphere. Defaults to :data:`~geovista.common.RADIUS`.
zlevel : int, default=0
The z-axis level. Used in combination with the `zscale` to offset the
`radius` by a proportional amount i.e., ``radius * zlevel * zscale``.
zscale : float, optional
The proportional multiplier for z-axis `zlevel`. Defaults to
:data:`~geovista.common.ZLEVEL_SCALE`.
clean : bool, optional
Specify whether to merge duplicate points, remove unused points,
and/or remove degenerate cells in the resultant mesh. See
:meth:`pyvista.PolyDataFilters.clean`. Defaults to
:data:`BRIDGE_CLEAN`.
Returns
-------
PolyData
The quad-faced spherical mesh.
Notes
-----
.. versionadded:: 0.1.0
"""
xs, ys = cls._as_contiguous_1d(xs, ys)
mxs, mys = np.meshgrid(xs, ys, indexing="xy")
return Transform.from_2d(
mxs,
mys,
data=data,
name=name,
crs=crs,
radius=radius,
zlevel=zlevel,
zscale=zscale,
clean=clean,
rgb=rgb,
)
[ドキュメント]
@classmethod
def from_2d(
cls,
xs: ArrayLike,
ys: ArrayLike,
/,
*,
data: ArrayLike | None = None,
name: str | None = None,
crs: CRSLike | None = None,
rgb: bool | None = False,
radius: float | None = None,
zlevel: int | None = None,
zscale: float | None = None,
clean: bool | None = None,
) -> pv.PolyData:
"""Build a quad-faced mesh from 2D x-values and y-values.
This allows the construction of a uniform, rectilinear or curvilinear
quad-faced ``(M, N)`` mesh grid, where the mesh has ``M``-faces in the y-axis,
and ``N``-faces in the x-axis, resulting in a mesh consisting of ``M*N`` faces.
The provided `xs` and `ys` define the four vertices of each quad-face
in the mesh for the native `crs`, which will then be projected to
geographic longitude and latitude values.
Parameters
----------
xs : ArrayLike
A 2D array of x-values, in canonical `crs` units, defining the
face x-value boundaries of the mesh. Creating a ``(M, N)`` mesh
requires a ``(M+1, N+1)`` x-axis array. Alternatively, a ``(M, N, 4)``
array may be provided.
ys : ArrayLike
A 2D array of y-values, in canonical `crs` units, defining the
face y-value boundaries of the mesh. Creating a ``(M, N)`` mesh
requires a ``(M+1, N+1)`` y-axis array. Alternatively, a ``(M, N, 4)``
array may be provided.
data : ArrayLike, optional
Data to be optionally attached to the mesh. The size of the data
must match either the shape of the fully formed mesh points
``(M+1)*(N+1)``,
or the number of mesh faces, ``M*N`` (but see the `rgb` parameter).
name : str, optional
The name of the optional data array to be attached to the mesh. If
`data` is provided but with no `name`, defaults to either
:data:`NAME_POINTS` or :data:`NAME_CELLS`.
crs : CRSLike, optional
The Coordinate Reference System of the provided `xs` and `ys`. May
be anything accepted by :meth:`pyproj.crs.CRS.from_user_input`. Defaults
to ``EPSG:4326`` i.e., ``WGS 84``.
rgb : bool, default=False
Whether `data` is an ``RGB`` or ``RGBA`` image. When ``rgb=True``,
`data` is expected to have an extra dimension for the colour
channels (length ``3`` or ``4``).
radius : float, optional
The radius of the sphere. Defaults to :data:`~geovista.common.RADIUS`.
zlevel : int, default=0
The z-axis level. Used in combination with the `zscale` to offset the
`radius` by a proportional amount i.e., ``radius * zlevel * zscale``.
zscale : float, optional
The proportional multiplier for z-axis `zlevel`. Defaults to
:data:`~geovista.common.ZLEVEL_SCALE`.
clean : bool, optional
Specify whether to merge duplicate points, remove unused points,
and/or remove degenerate cells in the resultant mesh. See
:meth:`pyvista.PolyDataFilters.clean`. Defaults to
:data:`BRIDGE_CLEAN`.
Returns
-------
PolyData
The quad-faced spherical mesh.
Notes
-----
.. versionadded:: 0.1.0
"""
xs, ys = np.asanyarray(xs), np.asanyarray(ys)
cls._verify_2d(xs, ys)
shape, ndim = xs.shape, xs.ndim
points_shape: tuple[int, int]
if ndim == 2:
# we have shape (M+1, N+1)
rows, cols = points_shape = shape
cells_shape = (rows - 1, cols - 1)
else:
# we have shape (M, N, 4)
rows, cols = cells_shape = shape[:-1]
points_shape = (rows + 1, cols + 1)
# generate connectivity (topology) map of indices into the geometry
connectivity = (
cls._create_connectivity_m1n1(points_shape)
if ndim == 2
else cls._create_connectivity_mn4(cells_shape)
)
return Transform.from_unstructured(
xs,
ys,
connectivity=connectivity,
data=data,
name=name,
crs=crs,
radius=radius,
zlevel=zlevel,
zscale=zscale,
clean=clean,
rgb=rgb,
)
[ドキュメント]
@classmethod
def from_points(
cls,
xs: ArrayLike,
ys: ArrayLike,
/,
*,
data: ArrayLike | None = None,
name: str | None = None,
crs: CRSLike | None = None,
radius: float | None = None,
zlevel: int | ArrayLike | None = None,
zscale: float | None = None,
clean: bool | None = None,
vectors: VectorLike | None = None,
vectors_crs: CRSLike | None = None,
vectors_name: str | None = None,
) -> pv.PolyData:
"""Build a point-cloud mesh from x-values, y-values and z-levels.
Note that optional mesh `data` or `vectors` must be in the same order
as the spatial points.
Parameters
----------
xs : ArrayLike
A 1D, 2D or 3D array of point-cloud x-values, in canonical `crs` units.
Must have the same shape as the `ys`.
ys : ArrayLike
A 1D, 2D or 3D array of point-cloud y-values, in canonical `crs` units.
Must have the same shape as the `xs`.
data : ArrayLike, optional
Data to be optionally attached to the mesh points defined by
`xs` and `ys`.
name : str, optional
The name of the optional data array to be attached to the mesh. If `data`
is provided but with no `name`, defaults to :data:`NAME_POINTS`.
crs : CRSLike, optional
The Coordinate Reference System of the provided `xs` and `ys`. May
be anything accepted by :meth:`pyproj.crs.CRS.from_user_input`. Defaults
to ``EPSG:4326`` i.e., ``WGS 84``.
radius : float, optional
The radius of the mesh point-cloud. Defaults to
:data:`~geovista.common.RADIUS`.
zlevel : int or ArrayLike, default=0
The z-axis level. Used in combination with the `zscale` to offset the
`radius` by a proportional amount i.e., ``radius * zlevel * zscale``.
If `zlevel` is not a scalar, then its shape must match or broadcast
with the shape of the `xs` and `ys`.
zscale : float, optional
The proportional multiplier for z-axis `zlevel`. Defaults to
:data:`~geovista.common.ZLEVEL_SCALE`.
clean : bool, optional
Specify whether to merge duplicate points. See
:meth:`pyvista.PolyDataFilters.clean`. Defaults to
:data:`BRIDGE_CLEAN`.
vectors : VectorLike, optional
A tuple of 2 or 3 arrays of the same shape as `xs` and `ys`.
These give eastward (``U``), northward (``V``) and optionally
upward (``W``) vector components, which are converted to an ``[N, 3]``
array of 3D vectors and attached to the resultant mesh, see
`vectors_name`. This can be used to generate glyphs (such as arrows)
and streamlines.
vectors_crs : CRSLike, optional
The Coordinate Reference System of the provided `vectors`. May be anything
accepted by :meth:`pyproj.crs.CRS.from_user_input`. Defaults to the same
as `crs`. Note that `vectors_crs` only specifies horizontal orientation
of the vectors. Their magnitudes are always spatial distance, and the
vertical component is always radial (i.e., "upwards").
vectors_name : str, optional
The name of the vectors array to be attached to the mesh. If
`vectors` is provided but with no `vectors_name`, defaults to
:data:`NAME_VECTORS`.
Returns
-------
PolyData
The point-cloud mesh with optional vectors attached.
Notes
-----
.. versionadded:: 0.2.0
"""
if clean is None:
clean = BRIDGE_CLEAN
radius = RADIUS if radius is None else abs(float(radius))
zscale = ZLEVEL_SCALE if zscale is None else float(zscale)
if crs is None:
crs = WGS84
# transform spatial points to WGS84 with shape (M, 3) or (M, N, 3)
transformed = transform_points(src_crs=crs, tgt_crs=WGS84, xs=xs, ys=ys)
lons, lats = transformed[..., 0], transformed[..., 1]
# convert lat/lon to cartesian xyz
xyz = to_cartesian(lons, lats, radius=radius, zlevel=zlevel, zscale=zscale)
# create the point-cloud mesh
mesh = pv.PolyData(xyz)
# attach the pyproj crs serialized as ogc wkt
to_wkt(mesh, WGS84)
# attach the original base radius and zscale
mesh.field_data[GV_FIELD_RADIUS] = np.array([radius])
mesh.field_data[GV_FIELD_ZSCALE] = np.array([zscale])
# attach any optional data to the mesh
if data is not None:
data = cls._as_compatible_data(data, mesh.n_points, mesh.n_cells)
if not name:
name = NAME_POINTS
mesh.field_data[GV_FIELD_NAME] = np.array([name])
mesh[name] = data
if vectors is not None:
# TODO @pp-mo: This section is very long, consider refactor
if not vectors_name:
vectors_name = NAME_VECTORS
if (
not isinstance(vectors, Iterable) # type: ignore[redundant-expr]
or len(vectors) not in (2, 3)
):
emsg = "'vectors' must be an iterable of 2 or 3 array-likes."
raise ValueError(emsg)
# unpack vector components to UVW
components = [np.asanyarray(component) for component in vectors]
us, vs = components[:2]
ws = components[2] if len(components) == 3 else np.zeros_like(us)
if vectors_crs is None:
# Vectors CRS defaults to points CRS
# NOTE: vectors may have a different CRS than the input points.
# The only likely usage is for true-lat-lon winds with locations on a
# rotated grid or similar, but we probably do need to support that.
vectors_crs = crs
# sanity check the vectors crs
vectors_crs = pyproj.CRS.from_user_input(vectors_crs)
if vectors_crs == WGS84:
vector_xs = lons
vector_ys = lats
post_rotate_matrix = None
else:
# The supplied UVW vectors are for a "different" orientation
# than standard lat-lon, so will need rotating afterwards.
# We can only do this for specific CRS types where we know how
# to find its pole.
axis_info = getattr(vectors_crs, "axis_info", [])
valid = len(axis_info) >= 2 and all(
hasattr(x, "name") for x in axis_info
)
if valid:
axis_names = [axis.name for axis in axis_info]
valid = axis_names[:2] == ["Longitude", "Latitude"]
if not valid:
msg = (
"Cannot determine wind directions : Target CRS type is not "
f"supported for grid orientation decoding : {vectors_crs}."
)
raise ValueError(msg)
# For a CRS with longitude and latitude axes, its axes determine the
# east-wards and north-wards directions of surface-oriented vectors.
# This means we can calculate the xyz vectors for the input points in
# vector coordinates, and afterwards rotate them to the display.
# TODO @pp-mo: Support other common cases as needed, for example OSGB
# or Mercator. There may be no general solution.
# Calculate post-rotate matrix, so "vectors @= post_rotate_matrix"
# transform the equivalent of the x,y,z basis vectors
bases_x = np.array([0, 90, 0])
bases_y = np.array([0, 0, 90])
transformed_bases = transform_points(
src_crs=vectors_crs, tgt_crs=WGS84, xs=bases_x, ys=bases_y
)
cartesian_bases = to_cartesian(
transformed_bases[:, 0], transformed_bases[:, 1]
)
post_rotate_matrix = np.array(cartesian_bases).T
# We will also need points as lons+lats **in the vector CRS** to
# calculate the vectors (i.e., oriented to "its" northward + eastward).
vector_xs = xs
vector_ys = ys
if vectors_crs != crs:
transformed = transform_points(
src_crs=crs, tgt_crs=vectors_crs, xs=xs, ys=ys
)
vector_xs, vector_ys = transformed[:, 0], transformed[:, 1]
# TODO @pp-mo: Should we pass flattened arrays here, and reshape as-per
# the inputs (and xyz)? Not clear if multidimensional
# input is used or needed
xx, yy, zz = vectors_to_cartesian(
lons=vector_xs,
lats=vector_ys,
vectors=(us, vs, ws),
radius=radius,
zlevel=zlevel,
zscale=zscale,
)
mesh_vectors = np.vstack((xx, yy, zz)).T
if post_rotate_matrix is not None:
# At this point, vectors are correct xyz's for the original points
# in 'vector_crs', but not ready for plotting since the "true"
# point locations are different : hence apply "post-rotation".
# TODO @pp-mo: Replace np.dot with '@' when masked-array multiply bug
# bug is fixed, see https://github.com/numpy/numpy/issues/14992
mesh_vectors = np.dot(post_rotate_matrix, mesh_vectors.T).T
mesh[vectors_name] = mesh_vectors
mesh.set_active_vectors(vectors_name)
# clean the mesh
if clean:
mesh.clean(inplace=True)
return mesh
[ドキュメント]
@classmethod
def from_tiff(
cls,
fname: PathLike,
/,
*,
name: str | None = None,
band: int = 1,
rgb: bool | None = False,
sieve: bool | None = False,
size: int | None = None,
extract: bool | None = False,
radius: float | None = None,
zlevel: int | None = None,
zscale: float | None = None,
clean: bool | None = None,
) -> pv.PolyData:
"""Build a quad-faced mesh from the GeoTIFF.
Note that the GeoTIFF data will be located on the ``points``
of the resultant mesh.
Parameters
----------
fname : PathLike
The file path to the GeoTIFF.
name : str, optional
The name of the GeoTIFF data array to be attached to the mesh.
Defaults to :data:`NAME_POINTS`. Note that ``{units}`` may be
used as a placeholder for the units of the data array e.g.,
``"Elevation / {units}"``.
band : int, default=1
The band index to read from the GeoTIFF. Note that the `band`
index is one-based.
rgb : bool, default=False
Specify whether to read the GeoTIFF as an ``RGB`` or ``RGBA`` image.
When ``rgb=True``, the `band` index is ignored.
sieve : bool, default=False
Specify whether to sieve the GeoTIFF mask to remove small connected
regions. See :func:`rasterio.features.sieve` for more information.
size : int, optional
The size of the `sieve` filter. Defaults to :data:`RIO_SIEVE_SIZE`.
extract : bool, default=False
Specify whether to extract cells from the mesh with no masked points.
radius : float, optional
The radius of the mesh sphere. Defaults to :data:`~geovista.common.RADIUS`.
zlevel : int, default=0
The z-axis level. Used in combination with the `zscale` to offset the
`radius` by a proportional amount i.e., ``radius * zlevel * zscale``.
zscale : float, optional
The proportional multiplier for z-axis `zlevel`. Defaults to
:data:`~geovista.common.ZLEVEL_SCALE`.
clean : bool, optional
Specify whether to merge duplicate points, remove unused points,
and/or remove degenerate cells in the resultant mesh. See
:meth:`pyvista.PolyDataFilters.clean`. Defaults to
:data:`BRIDGE_CLEAN`.
Returns
-------
PolyData
The GeoTIFF spherical mesh.
Notes
-----
.. versionadded:: 0.5.0
.. attention:: Optional package dependency :mod:`rasterio` is required.
Examples
--------
>>> from geovista import GeoPlotter, Transform
>>> from geovista.pantry import fetch_raster
>>> from geovista.pantry.textures import natural_earth_1
Render the GeoTIFF ``RGB`` image as a geolocated mesh.
First, :func:`rasterio.features.sieve` the GeoTIFF image to remove several
unwanted masked regions within the interior of the image, due to a lack of
dynamic range in the ``uint8`` image data. Then, extract the mesh to remove
cells with no masked points.
>>> fname = fetch_raster("bahamas_rgb.tif")
>>> mesh = Transform.from_tiff(fname, rgb=True, sieve=True, extract=True)
Now render the result:
>>> p = GeoPlotter()
>>> _ = p.add_mesh(mesh, rgb=True)
>>> p.view_poi()
>>> _ = p.add_base_layer(texture=natural_earth_1(), zlevel=0)
>>> _ = p.add_coastlines(color="white")
>>> p.show()
"""
try:
import rasterio as rio # noqa: PLC0415
except ImportError:
emsg = (
"Optional dependency 'rasterio' is required to read GeoTIFF files. "
"Use pip or conda to install."
)
raise ImportError(emsg) from None
if isinstance(fname, str):
fname = pathlib.Path(fname)
fname = fname.resolve(strict=True)
if size is None:
size = RIO_SIEVE_SIZE
with rio.open(fname, mode="r") as src:
count = src.count
if rgb:
if count not in [3, 4]:
plural = "s" if count > 1 else ""
emsg = (
f"Require a GeoTIFF with 3 or 4 bands to read as "
f"an RGB or RGBA image, only {count} band{plural} "
"available."
)
raise ValueError(emsg)
elif band < 1 or band > count:
if count == 1:
emsg = f"Require a band index of 1, got '{band}'."
else:
emsg = (
f"Require a band index in the closed interval [1, {count}], "
f"got '{band}'."
)
raise ValueError(emsg)
if name is not None:
name = str(name)
if "{units}" in name:
units = str(src.units[0] if rgb else src.units[band - 1])
name = name.format(units=units)
data = src.read(masked=extract) if rgb else src.read(band, masked=extract)
if extract:
# ignore the mask on the alpha channel, if present
mask = data[0].mask & data[1].mask & data[2].mask if rgb else data.mask
# ensure there is masked data prior to extracting unmasked points
extract = np.sum(mask) > 0
data = data.data
if rgb:
data = np.dstack(data).reshape(-1, count)
# transform from pixel offsets to crs coordinates
cols, rows = np.meshgrid(
np.arange(src.width), np.arange(src.height), indexing="xy"
)
# rasterio 1.4.0 (regression) expects 1D arrays, fixed in 1.4.1
# see https://github.com/rasterio/rasterio/issues/3191
xs, ys = rio.transform.xy(src.transform, rows.flatten(), cols.flatten())
# ensure we have arrays, rather than a list of arrays
xs, ys = np.asanyarray(xs), np.asanyarray(ys)
# ensure shape is maintained (rasterio 1.4.1 regression)
if xs.shape != src.shape:
xs = xs.reshape(src.shape)
if ys.shape != src.shape:
ys = ys.reshape(src.shape)
# create the geotiff mesh
mesh = cls.from_2d(
xs,
ys,
data=data,
name=name,
crs=src.crs,
rgb=rgb,
radius=radius,
zlevel=zlevel,
zscale=zscale,
clean=clean,
)
if extract:
if sieve:
from rasterio.features import sieve as riosieve # noqa: PLC0415
# convert boolean mask to conform to GDAL RFC 15 for sieve
# see https://gdal.org/en/stable/development/rfc/rfc15_nodatabitmask.html
dtype = np.dtype(src.dtypes[0])
muint = 2 ** (dtype.itemsize * 8) - 1
mask = (~mask * muint).astype(dtype)
mask = riosieve(mask, size=size)
# convert back to boolean mask
mask = ~mask.astype(bool)
# extract cells with no masked points
mesh = mesh.extract_points(~np.ravel(mask), adjacent_cells=False)
mesh = cast_UnstructuredGrid_to_PolyData(mesh)
return mesh
[ドキュメント]
@classmethod
def from_unstructured(
cls,
xs: ArrayLike,
ys: ArrayLike,
/,
*,
connectivity: ArrayLike | Shape | None = None,
data: ArrayLike | None = None,
start_index: int | None = None,
name: str | None = None,
crs: CRSLike | None = None,
rgb: bool | None = False,
radius: float | None = None,
zlevel: int | None = None,
zscale: float | None = None,
clean: bool | None = None,
) -> pv.PolyData:
"""Build a mesh from unstructured 1D x-values and y-values.
The `connectivity` defines the topology of faces within the
unstructured mesh. This is represented in terms of indices into the
provided `xs` and `ys` mesh geometry.
Note that any optional mesh `data` provided must be in the same order
as the mesh face `connectivity`, or in the same order as the points
described by `xs` and `ys` (data can only be attached to points or cells).
Parameters
----------
xs : ArrayLike
A 1D array of x-values, in canonical `crs` units, defining the
vertices of each face in the mesh.
ys : ArrayLike
A 1D array of y-values, in canonical `crs` units, defining the
vertices of each face in the mesh.
connectivity : ArrayLike or Shape, optional
Defines the topology of each face in the unstructured mesh in terms
of indices into the provided `xs` and `ys` mesh geometry
arrays. The `connectivity` is a 2D ``(M, N)`` array, where ``M`` is
the number of mesh faces, and ``N`` is the number of nodes per
face. Alternatively, an ``(M, N)`` tuple defining the connectivity
shape may be provided instead, given that the `xs` and `ys` define
``M*N`` points (at most) in the mesh geometry. If no connectivity is
provided, and the `xs` and `ys` are 2D, then their shape is used
to determine the connectivity. Also, note that masked connectivity
may be used to define a mesh consisting of different shaped faces.
data : ArrayLike, optional
Data to be optionally attached to the mesh face or nodes.
start_index : int, default=0
Specify the base index of the provided `connectivity` in the
closed interval [0, 1]. For example, if `start_index=1`, then
the `start_index` will be subtracted from the `connectivity`
to result in 0-based indices into the provided mesh geometry.
If no `start_index` is provided, then it will be determined
from the `connectivity`.
name : str, optional
The name of the optional data array to be attached to the mesh. If
`data` is provided but with no `name`, defaults to either
:data:`NAME_POINTS` or :data:`NAME_CELLS`.
crs : CRSLike, optional
The Coordinate Reference System of the provided `xs` and `ys`. May
be anything accepted by :meth:`pyproj.crs.CRS.from_user_input`. Defaults
to ``EPSG:4326`` i.e., ``WGS 84``.
rgb : bool, default=False
Whether `data` is an ``RGB`` or ``RGBA`` image. When ``rgb=True``,
`data` is expected to have an extra dimension for the colour
channels (length ``3`` or ``4``).
radius : float, optional
The radius of the mesh sphere. Defaults to :data:`~geovista.common.RADIUS`.
zlevel : int, default=0
The z-axis level. Used in combination with the `zscale` to offset the
`radius` by a proportional amount i.e., ``radius * zlevel * zscale``.
zscale : float, optional
The proportional multiplier for z-axis `zlevel`. Defaults to
:data:`~geovista.common.ZLEVEL_SCALE`.
clean : bool, optional
Specify whether to merge duplicate points, remove unused points,
and/or remove degenerate cells in the resultant mesh. See
:meth:`pyvista.PolyDataFilters.clean`. Defaults to
:data:`BRIDGE_CLEAN`.
Returns
-------
PolyData
The ``(M*N)``-faced spherical mesh.
Notes
-----
.. versionadded:: 0.1.0
"""
if rgb is None:
rgb = False
if clean is None:
clean = BRIDGE_CLEAN
xs, ys = np.asanyarray(xs), np.asanyarray(ys)
shape = xs.shape
if ys.shape != shape:
emsg = (
"Require x-values and y-values with the same shape, got "
f"'{shape}' and '{ys.shape}' respectively."
)
raise ValueError(emsg)
if xs.size < 3:
emsg = (
"Require a mesh to have at least one face with three "
"points/vertices i.e., minimal shape '(3,)', got "
f"x-values/y-values with shape '{shape}'."
)
raise ValueError(emsg)
# flatten the points to 1D with shape (M,)
xs, ys = xs.ravel(), ys.ravel()
if crs is None:
crs = WGS84
# transform spatial points to WGS84 with shape (M, 3)
transformed = transform_points(src_crs=crs, tgt_crs=WGS84, xs=xs, ys=ys)
xs, ys = transformed[:, 0], transformed[:, 1]
if connectivity is None:
# default to the shape of the points
connectivity = shape
if isinstance(connectivity, tuple):
ignore_start_index = True
npts = np.prod(connectivity)
dtype = np.uint32
if npts != xs.size:
emsg = (
f"Connectivity with shape '{connectivity}' requires "
f"'{npts:,d}' x-values/y-values, but only "
f"'{xs.size:,d}' have been provided."
)
raise ValueError(emsg)
# generate connectivity array
if (
np.ma.is_masked(xs)
and np.ma.is_masked(ys)
and np.array_equal(xs.mask, ys.mask)
):
connectivity_array = np.ma.arange(npts, dtype=dtype).reshape(
connectivity
)
connectivity_array.mask = np.copy(xs.mask)
else:
connectivity_array = np.arange(npts, dtype=dtype).reshape(connectivity)
else:
ignore_start_index = False
# copy connectivity to avoid memory corruption within vtk
connectivity_array = np.asanyarray(connectivity).copy()
cls._verify_connectivity(connectivity_array.shape)
if not ignore_start_index:
if start_index is None:
start_index = connectivity_array.min()
if start_index not in [0, 1]:
emsg = (
"Require a 'start_index' in the closed interval [0, 1], got "
f"'{start_index}'."
)
raise ValueError(emsg)
if start_index:
connectivity_array -= start_index
radius = RADIUS if radius is None else abs(float(radius))
zscale = ZLEVEL_SCALE if zscale is None else float(zscale)
zlevel = 0 if zlevel is None else int(zlevel)
radius += radius * zlevel * zscale
# convert lat/lon to cartesian xyz
geometry = to_cartesian(xs, ys, radius=radius)
if np.ma.is_masked(connectivity_array):
# create face connectivity from masked vertex indices, thus
# supporting varied mesh face geometry e.g., triangular, quad,
# pentagon (et al) cells within a single mesh.
connectivity_array = np.atleast_2d(connectivity_array)
if (ndim := connectivity_array.ndim) > 2:
emsg = f"Masked connectivity must be at most 2D, got {ndim}D."
raise ValueError(emsg)
n_faces = connectivity_array.shape[0]
n_vertices = np.ma.sum(~connectivity_array.mask, axis=1)
# ensure at least three vertices per face
valid_faces_mask = n_vertices > 2
if not np.all(valid_faces_mask):
n_invalid = n_faces - np.sum(valid_faces_mask)
plural = "s" if n_invalid > 1 else ""
wmsg = (
f"geovista masked connectivity defines {n_invalid:,} face{plural} "
"with no vertices."
)
warnings.warn(wmsg, stacklevel=2)
n_vertices = n_vertices[valid_faces_mask]
connectivity_array = connectivity_array[valid_faces_mask]
faces = np.ma.hstack(
[n_vertices.reshape(-1, 1), connectivity_array]
).ravel()
faces = faces[~faces.mask].data
else:
# create face connectivity serialization e.g., for a quad-mesh,
# each face we have (4, V0, V1, V2, V3), where "4" is the number
# of vertices defining the face, followed by the four indices (Vn)
# specifying each of the face vertices in an anti-clockwise order
# into the mesh geometry.
n_faces, n_vertices = connectivity_array.shape
faces = np.hstack(
[
np.broadcast_to(
np.array([n_vertices], dtype=np.int8), (n_faces, 1)
),
connectivity_array,
]
)
# create the mesh
mesh = pv.PolyData(geometry, faces=faces)
# attach the pyproj crs serialized as ogc wkt
to_wkt(mesh, WGS84)
# attach the radius
mesh.field_data[GV_FIELD_RADIUS] = np.array([radius])
# attach any optional data to the mesh
if data is not None:
data = cls._as_compatible_data(data, mesh.n_points, mesh.n_cells, rgb=rgb)
if not name:
size = data.size // data.shape[-1] if rgb else data.size
name = NAME_POINTS if size == mesh.n_points else NAME_CELLS
mesh.field_data[GV_FIELD_NAME] = np.array([name])
mesh[name] = data
# clean the mesh
if clean:
mesh.clean(inplace=True)
return mesh
[ドキュメント]
@classmethod
def to_structured_grid(
cls,
xs: ArrayLike,
ys: ArrayLike,
zs: ArrayLike,
/,
*,
data: ArrayLike | None = None,
name: str | None = None,
crs: CRSLike | None = None,
radius: float | None = None,
zlevel: float | None = None,
zscale: float | None = None,
) -> pv.StructuredGrid:
"""Build a structured grid from contiguous 1D spatial coordinates.
The 3D structured grid consists of cubic voxel cells which are
constructed from the 1D spatial coordinates defining the bounds of
the cells in each dimension.
Parameters
----------
xs : ArrayLike
The 1D array of x-values, in canonical `crs` units, defining the
x-axis cell bounds of the grid.
ys : ArrayLike
The 1D array of y-values, in canonical `crs` units, defining the
y-axis cell bounds of the grid.
zs : ArrayLike
The 1D array of z-values, in canonical `crs` units, defining the
z-axis cell bounds of the grid.
data : ArrayLike | None, optional
Data to be optionally attached to the structured grid cells or points.
Note that the expected data dimensionality is ``(Z, Y, X)`` order.
name : str | None, optional
The name of the optional data array to be attached to the grid. If
`data` is provided but with no `name`, defaults to either
:data:`NAME_POINTS` or :data:`NAME_CELLS`.
crs : CRSLike, optional
The Coordinate Reference System of the provided spatial coordinates.
May be anything accepted by :meth:`pyproj.crs.CRS.from_user_input`.
Defaults to ``EPSG:4326`` i.e., ``WGS 84``.
radius : float | None, optional
The radius of the grid sphere. Defaults to :data:`~geovista.common.RADIUS`.
zlevel : int, default=0
The z-axis level. Used in combination with the `zscale` to offset the
`radius` by a proportional amount i.e., ``radius * zlevel * zscale``.
zscale : float, optional
The proportional multiplier for z-axis `zlevel`. Defaults to
:data:`~geovista.common.ZLEVEL_SCALE`.
Returns
-------
StructuredGrid
The spherical structured grid.
Notes
-----
.. versionadded:: 0.6.0
"""
xs, ys, zs = np.atleast_1d(xs), np.atleast_1d(ys), np.atleast_1d(zs)
if xs.size < 2 or ys.size < 2 or zs.size < 2:
emsg = (
"Require at least two points per dimension to create a "
f"minimal structured grid voxel, got '{xs.size=}', "
f"'{ys.size=}' and '{zs.size=}'."
)
raise ValueError(emsg)
if xs.ndim != 1 or ys.ndim != 1 or zs.ndim != 1:
emsg = (
"Require 1D 'xs', 'ys' and 'zs', got "
f"'{xs.ndim}D', '{ys.ndim}D' and {zs.ndim}D'."
)
raise ValueError(emsg)
# TODO @bjlittle: add foreign crs to WGS84 support
if crs is None:
crs = WGS84
# TODO @bjlittle: add foreign crs to WGS84 support,
# require to enable 3D transforms
if crs != WGS84:
emsg = (
"Only spatial coordinates with a 'WGS84' CRS are supported"
"at the moment."
)
raise ValueError(emsg)
radius = RADIUS if radius is None else abs(float(radius))
zscale = ZLEVEL_SCALE if zscale is None else float(zscale)
zlevel = 0 if zlevel is None else int(zlevel)
# TODO @bjlittle: add richer vertical support
arc_length = np.radians(np.mean(np.diff(ys)))
zs = np.arange(*zs.shape) * arc_length * 0.5 + zlevel * zscale
# (M,), (N,), (P,) -> gives shapes (M, N, P)
xv, yv, zv = np.meshgrid(xs, ys, zs, indexing="ij")
shape = xv.shape
# convert lat/lon to cartesian xyz
xyz = to_cartesian(xv, yv, radius=radius, zlevel=zv, zscale=1)
# create the grid
grid = pv.StructuredGrid(
xyz[:, 0].reshape(shape), xyz[:, 1].reshape(shape), xyz[:, 2].reshape(shape)
)
# attach the pyproj crs serialized as ogc wkt
to_wkt(grid, WGS84)
if data is not None:
data = cls._as_compatible_data(data, grid.n_points, grid.n_cells)
if not name:
name = NAME_POINTS if data.size == grid.n_points else NAME_CELLS
grid.field_data[GV_FIELD_NAME] = np.array([name])
grid[name] = data
return grid
def __init__(
self,
xs: ArrayLike,
ys: ArrayLike,
/,
*,
connectivity: ArrayLike | Shape | None = None,
start_index: int | None = None,
crs: CRSLike | None = None,
radius: float | None = None,
zlevel: int | None = None,
zscale: float | None = None,
clean: bool | None = None,
) -> None:
"""Build a mesh from spatial points, connectivity, data and CRS metadata.
Convenience factory to build multiple identical meshes but with different face
or node data.
Parameters
----------
xs : ArrayLike
A 1D array of x-values, in canonical `crs` units, defining the
vertices of each face in the mesh.
ys : ArrayLike
A 1D array of y-values, in canonical `crs` units, defining the
vertices of each face in the mesh.
connectivity : ArrayLike or Shape, optional
Defines the topology of each face in the unstructured mesh in terms
of indices into the provided `xs` and `ys` mesh geometry
arrays. The `connectivity` is a 2D ``(M, N)`` array, where ``M`` is
the number of mesh faces, and ``N`` is the number of nodes per
face. Alternatively, an ``(M, N)`` tuple defining the connectivity
shape may be provided instead, given that the `xs` and `ys` define
``M*N`` points (at most) in the mesh geometry. If no connectivity is
provided, and the `xs` and `ys` are 2D, then their shape is used
to determine the connectivity. Also, note that masked connectivity
may be used to define a mesh consisting of different shaped faces.
start_index : int, default=0
Specify the base index of the provided `connectivity` in the
closed interval [0, 1]. For example, if `start_index=1`, then
the `start_index` will be subtracted from the `connectivity`
to result in 0-based indices into the provided mesh geometry.
If no `start_index` is provided, then it will be determined
from the `connectivity`.
crs : CRSLike, optional
The Coordinate Reference System of the provided `xs` and `ys`. May
be anything accepted by :meth:`pyproj.crs.CRS.from_user_input`. Defaults
to ``EPSG:4326`` i.e., ``WGS 84``.
radius : float, optional
The radius of the mesh sphere. Defaults to :data:`~geovista.common.RADIUS`.
zlevel : int, default=0
The z-axis level. Used in combination with the `zscale` to offset the
`radius` by a proportional amount i.e., ``radius * zlevel * zscale``.
zscale : float, optional
The proportional multiplier for z-axis `zlevel`. Defaults to
:data:`~geovista.common.ZLEVEL_SCALE`.
clean : bool, optional
Specify whether to merge duplicate points, remove unused points,
and/or remove degenerate cells in the resultant mesh. See
:meth:`pyvista.PolyDataFilters.clean`. Defaults to
:data:`BRIDGE_CLEAN`.
Notes
-----
.. versionadded:: 0.1.0
"""
xs, ys = np.asanyarray(xs), np.asanyarray(ys)
if connectivity is None:
if xs.ndim <= 1 or ys.ndim <= 1:
mesh = self.from_1d(
xs,
ys,
crs=crs,
radius=radius,
zlevel=zlevel,
zscale=zscale,
clean=clean,
)
else:
mesh = self.from_2d(
xs,
ys,
crs=crs,
radius=radius,
zlevel=zlevel,
zscale=zscale,
clean=clean,
)
else:
mesh = self.from_unstructured(
xs,
ys,
connectivity=connectivity,
start_index=start_index,
crs=crs,
radius=radius,
clean=clean,
zlevel=zlevel,
zscale=zscale,
)
self._mesh = mesh
self._n_points = mesh.n_points
self._n_cells = mesh.n_cells
[ドキュメント]
def __call__(
self, *, data: ArrayLike | None = None, name: str | None = None
) -> pv.PolyData:
"""Build the mesh and attach the provided `data` to faces or nodes.
Parameters
----------
data : ArrayLike, optional
Data to be optionally attached to the mesh face or nodes.
name : str, optional
The name of the optional data array to be attached to the mesh. If
`data` is provided but with no `name`, defaults to either
:data:`NAME_POINTS` or :data:`NAME_CELLS`.
Returns
-------
PolyData
The spherical mesh.
Notes
-----
..versionadded:: 0.1.0
"""
if data is not None:
data = self._as_compatible_data(data, self._n_points, self._n_cells)
mesh = pv.PolyData()
mesh.copy_structure(self._mesh)
if data is not None:
if not name:
name = NAME_POINTS if data.size == self._n_points else NAME_CELLS
mesh.field_data[GV_FIELD_NAME] = np.array([name])
mesh[name] = data
return mesh