|
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: |
|
|
|
|
|
self.triangles, self._neighbors = _qhull.delaunay(x, y, sys.flags.verbose) |
|
self.is_delaunay = True |
|
else: |
|
|
|
|
|
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()}') |
|
|
|
|
|
self._cpp_triangulation = None |
|
|
|
|
|
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( |
|
|
|
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 |
|
|
|
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:] |
|
|
|
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: |
|
|
|
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') |
|
|
|
|
|
if self._cpp_triangulation is not None: |
|
self._cpp_triangulation.set_mask( |
|
self.mask if self.mask is not None else ()) |
|
|
|
|
|
self._edges = None |
|
self._neighbors = None |
|
|
|
|
|
if self._trifinder is not None: |
|
self._trifinder._initialize() |
|
|