248 lines
9.6 KiB
Python
248 lines
9.6 KiB
Python
|
import sys
|
||
|
|
||
|
import numpy as np
|
||
|
|
||
|
from matplotlib import _api
|
||
|
|
||
|
|
||
|
class Triangulation:
|
||
|
"""
|
||
|
An unstructured triangular grid consisting of npoints points and
|
||
|
ntri triangles. The triangles can either be specified by the user
|
||
|
or automatically generated using a Delaunay triangulation.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
x, y : (npoints,) array-like
|
||
|
Coordinates of grid points.
|
||
|
triangles : (ntri, 3) array-like of int, optional
|
||
|
For each triangle, the indices of the three points that make
|
||
|
up the triangle, ordered in an anticlockwise manner. If not
|
||
|
specified, the Delaunay triangulation is calculated.
|
||
|
mask : (ntri,) array-like of bool, optional
|
||
|
Which triangles are masked out.
|
||
|
|
||
|
Attributes
|
||
|
----------
|
||
|
triangles : (ntri, 3) array of int
|
||
|
For each triangle, the indices of the three points that make
|
||
|
up the triangle, ordered in an anticlockwise manner. If you want to
|
||
|
take the *mask* into account, use `get_masked_triangles` instead.
|
||
|
mask : (ntri, 3) array of bool or None
|
||
|
Masked out triangles.
|
||
|
is_delaunay : bool
|
||
|
Whether the Triangulation is a calculated Delaunay
|
||
|
triangulation (where *triangles* was not specified) or not.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
For a Triangulation to be valid it must not have duplicate points,
|
||
|
triangles formed from colinear points, or overlapping triangles.
|
||
|
"""
|
||
|
def __init__(self, x, y, triangles=None, mask=None):
|
||
|
from matplotlib import _qhull
|
||
|
|
||
|
self.x = np.asarray(x, dtype=np.float64)
|
||
|
self.y = np.asarray(y, dtype=np.float64)
|
||
|
if self.x.shape != self.y.shape or self.x.ndim != 1:
|
||
|
raise ValueError("x and y must be equal-length 1D arrays, but "
|
||
|
f"found shapes {self.x.shape!r} and "
|
||
|
f"{self.y.shape!r}")
|
||
|
|
||
|
self.mask = None
|
||
|
self._edges = None
|
||
|
self._neighbors = None
|
||
|
self.is_delaunay = False
|
||
|
|
||
|
if triangles is None:
|
||
|
# No triangulation specified, so use matplotlib._qhull to obtain
|
||
|
# Delaunay triangulation.
|
||
|
self.triangles, self._neighbors = _qhull.delaunay(x, y, sys.flags.verbose)
|
||
|
self.is_delaunay = True
|
||
|
else:
|
||
|
# Triangulation specified. Copy, since we may correct triangle
|
||
|
# orientation.
|
||
|
try:
|
||
|
self.triangles = np.array(triangles, dtype=np.int32, order='C')
|
||
|
except ValueError as e:
|
||
|
raise ValueError('triangles must be a (N, 3) int array, not '
|
||
|
f'{triangles!r}') from e
|
||
|
if self.triangles.ndim != 2 or self.triangles.shape[1] != 3:
|
||
|
raise ValueError(
|
||
|
'triangles must be a (N, 3) int array, but found shape '
|
||
|
f'{self.triangles.shape!r}')
|
||
|
if self.triangles.max() >= len(self.x):
|
||
|
raise ValueError(
|
||
|
'triangles are indices into the points and must be in the '
|
||
|
f'range 0 <= i < {len(self.x)} but found value '
|
||
|
f'{self.triangles.max()}')
|
||
|
if self.triangles.min() < 0:
|
||
|
raise ValueError(
|
||
|
'triangles are indices into the points and must be in the '
|
||
|
f'range 0 <= i < {len(self.x)} but found value '
|
||
|
f'{self.triangles.min()}')
|
||
|
|
||
|
# Underlying C++ object is not created until first needed.
|
||
|
self._cpp_triangulation = None
|
||
|
|
||
|
# Default TriFinder not created until needed.
|
||
|
self._trifinder = None
|
||
|
|
||
|
self.set_mask(mask)
|
||
|
|
||
|
def calculate_plane_coefficients(self, z):
|
||
|
"""
|
||
|
Calculate plane equation coefficients for all unmasked triangles from
|
||
|
the point (x, y) coordinates and specified z-array of shape (npoints).
|
||
|
The returned array has shape (npoints, 3) and allows z-value at (x, y)
|
||
|
position in triangle tri to be calculated using
|
||
|
``z = array[tri, 0] * x + array[tri, 1] * y + array[tri, 2]``.
|
||
|
"""
|
||
|
return self.get_cpp_triangulation().calculate_plane_coefficients(z)
|
||
|
|
||
|
@property
|
||
|
def edges(self):
|
||
|
"""
|
||
|
Return integer array of shape (nedges, 2) containing all edges of
|
||
|
non-masked triangles.
|
||
|
|
||
|
Each row defines an edge by its start point index and end point
|
||
|
index. Each edge appears only once, i.e. for an edge between points
|
||
|
*i* and *j*, there will only be either *(i, j)* or *(j, i)*.
|
||
|
"""
|
||
|
if self._edges is None:
|
||
|
self._edges = self.get_cpp_triangulation().get_edges()
|
||
|
return self._edges
|
||
|
|
||
|
def get_cpp_triangulation(self):
|
||
|
"""
|
||
|
Return the underlying C++ Triangulation object, creating it
|
||
|
if necessary.
|
||
|
"""
|
||
|
from matplotlib import _tri
|
||
|
if self._cpp_triangulation is None:
|
||
|
self._cpp_triangulation = _tri.Triangulation(
|
||
|
# For unset arrays use empty tuple which has size of zero.
|
||
|
self.x, self.y, self.triangles,
|
||
|
self.mask if self.mask is not None else (),
|
||
|
self._edges if self._edges is not None else (),
|
||
|
self._neighbors if self._neighbors is not None else (),
|
||
|
not self.is_delaunay)
|
||
|
return self._cpp_triangulation
|
||
|
|
||
|
def get_masked_triangles(self):
|
||
|
"""
|
||
|
Return an array of triangles taking the mask into account.
|
||
|
"""
|
||
|
if self.mask is not None:
|
||
|
return self.triangles[~self.mask]
|
||
|
else:
|
||
|
return self.triangles
|
||
|
|
||
|
@staticmethod
|
||
|
def get_from_args_and_kwargs(*args, **kwargs):
|
||
|
"""
|
||
|
Return a Triangulation object from the args and kwargs, and
|
||
|
the remaining args and kwargs with the consumed values removed.
|
||
|
|
||
|
There are two alternatives: either the first argument is a
|
||
|
Triangulation object, in which case it is returned, or the args
|
||
|
and kwargs are sufficient to create a new Triangulation to
|
||
|
return. In the latter case, see Triangulation.__init__ for
|
||
|
the possible args and kwargs.
|
||
|
"""
|
||
|
if isinstance(args[0], Triangulation):
|
||
|
triangulation, *args = args
|
||
|
if 'triangles' in kwargs:
|
||
|
_api.warn_external(
|
||
|
"Passing the keyword 'triangles' has no effect when also "
|
||
|
"passing a Triangulation")
|
||
|
if 'mask' in kwargs:
|
||
|
_api.warn_external(
|
||
|
"Passing the keyword 'mask' has no effect when also "
|
||
|
"passing a Triangulation")
|
||
|
else:
|
||
|
x, y, triangles, mask, args, kwargs = \
|
||
|
Triangulation._extract_triangulation_params(args, kwargs)
|
||
|
triangulation = Triangulation(x, y, triangles, mask)
|
||
|
return triangulation, args, kwargs
|
||
|
|
||
|
@staticmethod
|
||
|
def _extract_triangulation_params(args, kwargs):
|
||
|
x, y, *args = args
|
||
|
# Check triangles in kwargs then args.
|
||
|
triangles = kwargs.pop('triangles', None)
|
||
|
from_args = False
|
||
|
if triangles is None and args:
|
||
|
triangles = args[0]
|
||
|
from_args = True
|
||
|
if triangles is not None:
|
||
|
try:
|
||
|
triangles = np.asarray(triangles, dtype=np.int32)
|
||
|
except ValueError:
|
||
|
triangles = None
|
||
|
if triangles is not None and (triangles.ndim != 2 or
|
||
|
triangles.shape[1] != 3):
|
||
|
triangles = None
|
||
|
if triangles is not None and from_args:
|
||
|
args = args[1:] # Consumed first item in args.
|
||
|
# Check for mask in kwargs.
|
||
|
mask = kwargs.pop('mask', None)
|
||
|
return x, y, triangles, mask, args, kwargs
|
||
|
|
||
|
def get_trifinder(self):
|
||
|
"""
|
||
|
Return the default `matplotlib.tri.TriFinder` of this
|
||
|
triangulation, creating it if necessary. This allows the same
|
||
|
TriFinder object to be easily shared.
|
||
|
"""
|
||
|
if self._trifinder is None:
|
||
|
# Default TriFinder class.
|
||
|
from matplotlib.tri._trifinder import TrapezoidMapTriFinder
|
||
|
self._trifinder = TrapezoidMapTriFinder(self)
|
||
|
return self._trifinder
|
||
|
|
||
|
@property
|
||
|
def neighbors(self):
|
||
|
"""
|
||
|
Return integer array of shape (ntri, 3) containing neighbor triangles.
|
||
|
|
||
|
For each triangle, the indices of the three triangles that
|
||
|
share the same edges, or -1 if there is no such neighboring
|
||
|
triangle. ``neighbors[i, j]`` is the triangle that is the neighbor
|
||
|
to the edge from point index ``triangles[i, j]`` to point index
|
||
|
``triangles[i, (j+1)%3]``.
|
||
|
"""
|
||
|
if self._neighbors is None:
|
||
|
self._neighbors = self.get_cpp_triangulation().get_neighbors()
|
||
|
return self._neighbors
|
||
|
|
||
|
def set_mask(self, mask):
|
||
|
"""
|
||
|
Set or clear the mask array.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
mask : None or bool array of length ntri
|
||
|
"""
|
||
|
if mask is None:
|
||
|
self.mask = None
|
||
|
else:
|
||
|
self.mask = np.asarray(mask, dtype=bool)
|
||
|
if self.mask.shape != (self.triangles.shape[0],):
|
||
|
raise ValueError('mask array must have same length as '
|
||
|
'triangles array')
|
||
|
|
||
|
# Set mask in C++ Triangulation.
|
||
|
if self._cpp_triangulation is not None:
|
||
|
self._cpp_triangulation.set_mask(
|
||
|
self.mask if self.mask is not None else ())
|
||
|
|
||
|
# Clear derived fields so they are recalculated when needed.
|
||
|
self._edges = None
|
||
|
self._neighbors = None
|
||
|
|
||
|
# Recalculate TriFinder if it exists.
|
||
|
if self._trifinder is not None:
|
||
|
self._trifinder._initialize()
|