This commit is contained in:
2024-11-29 18:15:30 +00:00
parent 40aade2d8e
commit bc9415586e
5298 changed files with 1938676 additions and 80 deletions

View File

@ -0,0 +1,23 @@
"""
Unstructured triangular grid functions.
"""
from ._triangulation import Triangulation
from ._tricontour import TriContourSet, tricontour, tricontourf
from ._trifinder import TriFinder, TrapezoidMapTriFinder
from ._triinterpolate import (TriInterpolator, LinearTriInterpolator,
CubicTriInterpolator)
from ._tripcolor import tripcolor
from ._triplot import triplot
from ._trirefine import TriRefiner, UniformTriRefiner
from ._tritools import TriAnalyzer
__all__ = ["Triangulation",
"TriContourSet", "tricontour", "tricontourf",
"TriFinder", "TrapezoidMapTriFinder",
"TriInterpolator", "LinearTriInterpolator", "CubicTriInterpolator",
"tripcolor",
"triplot",
"TriRefiner", "UniformTriRefiner",
"TriAnalyzer"]

View File

@ -0,0 +1,247 @@
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()

View File

@ -0,0 +1,33 @@
from matplotlib import _tri
from matplotlib.tri._trifinder import TriFinder
import numpy as np
from numpy.typing import ArrayLike
from typing import Any
class Triangulation:
x: np.ndarray
y: np.ndarray
mask: np.ndarray | None
is_delaunay: bool
triangles: np.ndarray
def __init__(
self,
x: ArrayLike,
y: ArrayLike,
triangles: ArrayLike | None = ...,
mask: ArrayLike | None = ...,
) -> None: ...
def calculate_plane_coefficients(self, z: ArrayLike) -> np.ndarray: ...
@property
def edges(self) -> np.ndarray: ...
def get_cpp_triangulation(self) -> _tri.Triangulation: ...
def get_masked_triangles(self) -> np.ndarray: ...
@staticmethod
def get_from_args_and_kwargs(
*args, **kwargs
) -> tuple[Triangulation, tuple[Any, ...], dict[str, Any]]: ...
def get_trifinder(self) -> TriFinder: ...
@property
def neighbors(self) -> np.ndarray: ...
def set_mask(self, mask: None | ArrayLike) -> None: ...

View File

@ -0,0 +1,270 @@
import numpy as np
from matplotlib import _docstring
from matplotlib.contour import ContourSet
from matplotlib.tri._triangulation import Triangulation
@_docstring.dedent_interpd
class TriContourSet(ContourSet):
"""
Create and store a set of contour lines or filled regions for
a triangular grid.
This class is typically not instantiated directly by the user but by
`~.Axes.tricontour` and `~.Axes.tricontourf`.
%(contour_set_attributes)s
"""
def __init__(self, ax, *args, **kwargs):
"""
Draw triangular grid contour lines or filled regions,
depending on whether keyword arg *filled* is False
(default) or True.
The first argument of the initializer must be an `~.axes.Axes`
object. The remaining arguments and keyword arguments
are described in the docstring of `~.Axes.tricontour`.
"""
super().__init__(ax, *args, **kwargs)
def _process_args(self, *args, **kwargs):
"""
Process args and kwargs.
"""
if isinstance(args[0], TriContourSet):
C = args[0]._contour_generator
if self.levels is None:
self.levels = args[0].levels
self.zmin = args[0].zmin
self.zmax = args[0].zmax
self._mins = args[0]._mins
self._maxs = args[0]._maxs
else:
from matplotlib import _tri
tri, z = self._contour_args(args, kwargs)
C = _tri.TriContourGenerator(tri.get_cpp_triangulation(), z)
self._mins = [tri.x.min(), tri.y.min()]
self._maxs = [tri.x.max(), tri.y.max()]
self._contour_generator = C
return kwargs
def _contour_args(self, args, kwargs):
tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args,
**kwargs)
z, *args = args
z = np.ma.asarray(z)
if z.shape != tri.x.shape:
raise ValueError('z array must have same length as triangulation x'
' and y arrays')
# z values must be finite, only need to check points that are included
# in the triangulation.
z_check = z[np.unique(tri.get_masked_triangles())]
if np.ma.is_masked(z_check):
raise ValueError('z must not contain masked points within the '
'triangulation')
if not np.isfinite(z_check).all():
raise ValueError('z array must not contain non-finite values '
'within the triangulation')
z = np.ma.masked_invalid(z, copy=False)
self.zmax = float(z_check.max())
self.zmin = float(z_check.min())
if self.logscale and self.zmin <= 0:
func = 'contourf' if self.filled else 'contour'
raise ValueError(f'Cannot {func} log of negative values.')
self._process_contour_level_args(args, z.dtype)
return (tri, z)
_docstring.interpd.update(_tricontour_doc="""
Draw contour %%(type)s on an unstructured triangular grid.
Call signatures::
%%(func)s(triangulation, z, [levels], ...)
%%(func)s(x, y, z, [levels], *, [triangles=triangles], [mask=mask], ...)
The triangular grid can be specified either by passing a `.Triangulation`
object as the first parameter, or by passing the points *x*, *y* and
optionally the *triangles* and a *mask*. See `.Triangulation` for an
explanation of these parameters. If neither of *triangulation* or
*triangles* are given, the triangulation is calculated on the fly.
It is possible to pass *triangles* positionally, i.e.
``%%(func)s(x, y, triangles, z, ...)``. However, this is discouraged. For more
clarity, pass *triangles* via keyword argument.
Parameters
----------
triangulation : `.Triangulation`, optional
An already created triangular grid.
x, y, triangles, mask
Parameters defining the triangular grid. See `.Triangulation`.
This is mutually exclusive with specifying *triangulation*.
z : array-like
The height values over which the contour is drawn. Color-mapping is
controlled by *cmap*, *norm*, *vmin*, and *vmax*.
.. note::
All values in *z* must be finite. Hence, nan and inf values must
either be removed or `~.Triangulation.set_mask` be used.
levels : int or array-like, optional
Determines the number and positions of the contour lines / regions.
If an int *n*, use `~matplotlib.ticker.MaxNLocator`, which tries to
automatically choose no more than *n+1* "nice" contour levels between
between minimum and maximum numeric values of *Z*.
If array-like, draw contour lines at the specified levels. The values must
be in increasing order.
Returns
-------
`~matplotlib.tri.TriContourSet`
Other Parameters
----------------
colors : :mpltype:`color` or list of :mpltype:`color`, optional
The colors of the levels, i.e., the contour %%(type)s.
The sequence is cycled for the levels in ascending order. If the sequence
is shorter than the number of levels, it is repeated.
As a shortcut, single color strings may be used in place of one-element
lists, i.e. ``'red'`` instead of ``['red']`` to color all levels with the
same color. This shortcut does only work for color strings, not for other
ways of specifying colors.
By default (value *None*), the colormap specified by *cmap* will be used.
alpha : float, default: 1
The alpha blending value, between 0 (transparent) and 1 (opaque).
%(cmap_doc)s
This parameter is ignored if *colors* is set.
%(norm_doc)s
This parameter is ignored if *colors* is set.
%(vmin_vmax_doc)s
If *vmin* or *vmax* are not given, the default color scaling is based on
*levels*.
This parameter is ignored if *colors* is set.
origin : {*None*, 'upper', 'lower', 'image'}, default: None
Determines the orientation and exact position of *z* by specifying the
position of ``z[0, 0]``. This is only relevant, if *X*, *Y* are not given.
- *None*: ``z[0, 0]`` is at X=0, Y=0 in the lower left corner.
- 'lower': ``z[0, 0]`` is at X=0.5, Y=0.5 in the lower left corner.
- 'upper': ``z[0, 0]`` is at X=N+0.5, Y=0.5 in the upper left corner.
- 'image': Use the value from :rc:`image.origin`.
extent : (x0, x1, y0, y1), optional
If *origin* is not *None*, then *extent* is interpreted as in `.imshow`: it
gives the outer pixel boundaries. In this case, the position of z[0, 0] is
the center of the pixel, not a corner. If *origin* is *None*, then
(*x0*, *y0*) is the position of z[0, 0], and (*x1*, *y1*) is the position
of z[-1, -1].
This argument is ignored if *X* and *Y* are specified in the call to
contour.
locator : ticker.Locator subclass, optional
The locator is used to determine the contour levels if they are not given
explicitly via *levels*.
Defaults to `~.ticker.MaxNLocator`.
extend : {'neither', 'both', 'min', 'max'}, default: 'neither'
Determines the ``%%(func)s``-coloring of values that are outside the
*levels* range.
If 'neither', values outside the *levels* range are not colored. If 'min',
'max' or 'both', color the values below, above or below and above the
*levels* range.
Values below ``min(levels)`` and above ``max(levels)`` are mapped to the
under/over values of the `.Colormap`. Note that most colormaps do not have
dedicated colors for these by default, so that the over and under values
are the edge values of the colormap. You may want to set these values
explicitly using `.Colormap.set_under` and `.Colormap.set_over`.
.. note::
An existing `.TriContourSet` does not get notified if properties of its
colormap are changed. Therefore, an explicit call to
`.ContourSet.changed()` is needed after modifying the colormap. The
explicit call can be left out, if a colorbar is assigned to the
`.TriContourSet` because it internally calls `.ContourSet.changed()`.
xunits, yunits : registered units, optional
Override axis units by specifying an instance of a
:class:`matplotlib.units.ConversionInterface`.
antialiased : bool, optional
Enable antialiasing, overriding the defaults. For
filled contours, the default is *True*. For line contours,
it is taken from :rc:`lines.antialiased`.""" % _docstring.interpd.params)
@_docstring.Substitution(func='tricontour', type='lines')
@_docstring.dedent_interpd
def tricontour(ax, *args, **kwargs):
"""
%(_tricontour_doc)s
linewidths : float or array-like, default: :rc:`contour.linewidth`
The line width of the contour lines.
If a number, all levels will be plotted with this linewidth.
If a sequence, the levels in ascending order will be plotted with
the linewidths in the order specified.
If None, this falls back to :rc:`lines.linewidth`.
linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, optional
If *linestyles* is *None*, the default is 'solid' unless the lines are
monochrome. In that case, negative contours will take their linestyle
from :rc:`contour.negative_linestyle` setting.
*linestyles* can also be an iterable of the above strings specifying a
set of linestyles to be used. If this iterable is shorter than the
number of contour levels it will be repeated as necessary.
"""
kwargs['filled'] = False
return TriContourSet(ax, *args, **kwargs)
@_docstring.Substitution(func='tricontourf', type='regions')
@_docstring.dedent_interpd
def tricontourf(ax, *args, **kwargs):
"""
%(_tricontour_doc)s
hatches : list[str], optional
A list of crosshatch patterns to use on the filled areas.
If None, no hatching will be added to the contour.
Notes
-----
`.tricontourf` fills intervals that are closed at the top; that is, for
boundaries *z1* and *z2*, the filled region is::
z1 < Z <= z2
except for the lowest interval, which is closed on both sides (i.e. it
includes the lowest value).
"""
kwargs['filled'] = True
return TriContourSet(ax, *args, **kwargs)

View File

@ -0,0 +1,52 @@
from matplotlib.axes import Axes
from matplotlib.contour import ContourSet
from matplotlib.tri._triangulation import Triangulation
from numpy.typing import ArrayLike
from typing import overload
# TODO: more explicit args/kwargs (for all things in this module)?
class TriContourSet(ContourSet):
def __init__(self, ax: Axes, *args, **kwargs) -> None: ...
@overload
def tricontour(
ax: Axes,
triangulation: Triangulation,
z: ArrayLike,
levels: int | ArrayLike = ...,
**kwargs
) -> TriContourSet: ...
@overload
def tricontour(
ax: Axes,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
levels: int | ArrayLike = ...,
*,
triangles: ArrayLike = ...,
mask: ArrayLike = ...,
**kwargs
) -> TriContourSet: ...
@overload
def tricontourf(
ax: Axes,
triangulation: Triangulation,
z: ArrayLike,
levels: int | ArrayLike = ...,
**kwargs
) -> TriContourSet: ...
@overload
def tricontourf(
ax: Axes,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
levels: int | ArrayLike = ...,
*,
triangles: ArrayLike = ...,
mask: ArrayLike = ...,
**kwargs
) -> TriContourSet: ...

View File

@ -0,0 +1,96 @@
import numpy as np
from matplotlib import _api
from matplotlib.tri import Triangulation
class TriFinder:
"""
Abstract base class for classes used to find the triangles of a
Triangulation in which (x, y) points lie.
Rather than instantiate an object of a class derived from TriFinder, it is
usually better to use the function `.Triangulation.get_trifinder`.
Derived classes implement __call__(x, y) where x and y are array-like point
coordinates of the same shape.
"""
def __init__(self, triangulation):
_api.check_isinstance(Triangulation, triangulation=triangulation)
self._triangulation = triangulation
def __call__(self, x, y):
raise NotImplementedError
class TrapezoidMapTriFinder(TriFinder):
"""
`~matplotlib.tri.TriFinder` class implemented using the trapezoid
map algorithm from the book "Computational Geometry, Algorithms and
Applications", second edition, by M. de Berg, M. van Kreveld, M. Overmars
and O. Schwarzkopf.
The triangulation must be valid, i.e. it must not have duplicate points,
triangles formed from colinear points, or overlapping triangles. The
algorithm has some tolerance to triangles formed from colinear points, but
this should not be relied upon.
"""
def __init__(self, triangulation):
from matplotlib import _tri
super().__init__(triangulation)
self._cpp_trifinder = _tri.TrapezoidMapTriFinder(
triangulation.get_cpp_triangulation())
self._initialize()
def __call__(self, x, y):
"""
Return an array containing the indices of the triangles in which the
specified *x*, *y* points lie, or -1 for points that do not lie within
a triangle.
*x*, *y* are array-like x and y coordinates of the same shape and any
number of dimensions.
Returns integer array with the same shape and *x* and *y*.
"""
x = np.asarray(x, dtype=np.float64)
y = np.asarray(y, dtype=np.float64)
if x.shape != y.shape:
raise ValueError("x and y must be array-like with the same shape")
# C++ does the heavy lifting, and expects 1D arrays.
indices = (self._cpp_trifinder.find_many(x.ravel(), y.ravel())
.reshape(x.shape))
return indices
def _get_tree_stats(self):
"""
Return a python list containing the statistics about the node tree:
0: number of nodes (tree size)
1: number of unique nodes
2: number of trapezoids (tree leaf nodes)
3: number of unique trapezoids
4: maximum parent count (max number of times a node is repeated in
tree)
5: maximum depth of tree (one more than the maximum number of
comparisons needed to search through the tree)
6: mean of all trapezoid depths (one more than the average number
of comparisons needed to search through the tree)
"""
return self._cpp_trifinder.get_tree_stats()
def _initialize(self):
"""
Initialize the underlying C++ object. Can be called multiple times if,
for example, the triangulation is modified.
"""
self._cpp_trifinder.initialize()
def _print_tree(self):
"""
Print a text representation of the node tree, which is useful for
debugging purposes.
"""
self._cpp_trifinder.print_tree()

View File

@ -0,0 +1,10 @@
from matplotlib.tri import Triangulation
from numpy.typing import ArrayLike
class TriFinder:
def __init__(self, triangulation: Triangulation) -> None: ...
def __call__(self, x: ArrayLike, y: ArrayLike) -> ArrayLike: ...
class TrapezoidMapTriFinder(TriFinder):
def __init__(self, triangulation: Triangulation) -> None: ...
def __call__(self, x: ArrayLike, y: ArrayLike) -> ArrayLike: ...

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
from matplotlib.tri import Triangulation, TriFinder
from typing import Literal
import numpy as np
from numpy.typing import ArrayLike
class TriInterpolator:
def __init__(
self,
triangulation: Triangulation,
z: ArrayLike,
trifinder: TriFinder | None = ...,
) -> None: ...
# __call__ and gradient are not actually implemented by the ABC, but are specified as required
def __call__(self, x: ArrayLike, y: ArrayLike) -> np.ma.MaskedArray: ...
def gradient(
self, x: ArrayLike, y: ArrayLike
) -> tuple[np.ma.MaskedArray, np.ma.MaskedArray]: ...
class LinearTriInterpolator(TriInterpolator): ...
class CubicTriInterpolator(TriInterpolator):
def __init__(
self,
triangulation: Triangulation,
z: ArrayLike,
kind: Literal["min_E", "geom", "user"] = ...,
trifinder: TriFinder | None = ...,
dz: tuple[ArrayLike, ArrayLike] | None = ...,
) -> None: ...

View File

@ -0,0 +1,149 @@
import numpy as np
from matplotlib import _api
from matplotlib.collections import PolyCollection, TriMesh
from matplotlib.tri._triangulation import Triangulation
def tripcolor(ax, *args, alpha=1.0, norm=None, cmap=None, vmin=None,
vmax=None, shading='flat', facecolors=None, **kwargs):
"""
Create a pseudocolor plot of an unstructured triangular grid.
Call signatures::
tripcolor(triangulation, c, *, ...)
tripcolor(x, y, c, *, [triangles=triangles], [mask=mask], ...)
The triangular grid can be specified either by passing a `.Triangulation`
object as the first parameter, or by passing the points *x*, *y* and
optionally the *triangles* and a *mask*. See `.Triangulation` for an
explanation of these parameters.
It is possible to pass the triangles positionally, i.e.
``tripcolor(x, y, triangles, c, ...)``. However, this is discouraged.
For more clarity, pass *triangles* via keyword argument.
If neither of *triangulation* or *triangles* are given, the triangulation
is calculated on the fly. In this case, it does not make sense to provide
colors at the triangle faces via *c* or *facecolors* because there are
multiple possible triangulations for a group of points and you don't know
which triangles will be constructed.
Parameters
----------
triangulation : `.Triangulation`
An already created triangular grid.
x, y, triangles, mask
Parameters defining the triangular grid. See `.Triangulation`.
This is mutually exclusive with specifying *triangulation*.
c : array-like
The color values, either for the points or for the triangles. Which one
is automatically inferred from the length of *c*, i.e. does it match
the number of points or the number of triangles. If there are the same
number of points and triangles in the triangulation it is assumed that
color values are defined at points; to force the use of color values at
triangles use the keyword argument ``facecolors=c`` instead of just
``c``.
This parameter is position-only.
facecolors : array-like, optional
Can be used alternatively to *c* to specify colors at the triangle
faces. This parameter takes precedence over *c*.
shading : {'flat', 'gouraud'}, default: 'flat'
If 'flat' and the color values *c* are defined at points, the color
values used for each triangle are from the mean c of the triangle's
three points. If *shading* is 'gouraud' then color values must be
defined at points.
other_parameters
All other parameters are the same as for `~.Axes.pcolor`.
"""
_api.check_in_list(['flat', 'gouraud'], shading=shading)
tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, **kwargs)
# Parse the color to be in one of (the other variable will be None):
# - facecolors: if specified at the triangle faces
# - point_colors: if specified at the points
if facecolors is not None:
if args:
_api.warn_external(
"Positional parameter c has no effect when the keyword "
"facecolors is given")
point_colors = None
if len(facecolors) != len(tri.triangles):
raise ValueError("The length of facecolors must match the number "
"of triangles")
else:
# Color from positional parameter c
if not args:
raise TypeError(
"tripcolor() missing 1 required positional argument: 'c'; or "
"1 required keyword-only argument: 'facecolors'")
elif len(args) > 1:
raise TypeError(f"Unexpected positional parameters: {args[1:]!r}")
c = np.asarray(args[0])
if len(c) == len(tri.x):
# having this before the len(tri.triangles) comparison gives
# precedence to nodes if there are as many nodes as triangles
point_colors = c
facecolors = None
elif len(c) == len(tri.triangles):
point_colors = None
facecolors = c
else:
raise ValueError('The length of c must match either the number '
'of points or the number of triangles')
# Handling of linewidths, shading, edgecolors and antialiased as
# in Axes.pcolor
linewidths = (0.25,)
if 'linewidth' in kwargs:
kwargs['linewidths'] = kwargs.pop('linewidth')
kwargs.setdefault('linewidths', linewidths)
edgecolors = 'none'
if 'edgecolor' in kwargs:
kwargs['edgecolors'] = kwargs.pop('edgecolor')
ec = kwargs.setdefault('edgecolors', edgecolors)
if 'antialiased' in kwargs:
kwargs['antialiaseds'] = kwargs.pop('antialiased')
if 'antialiaseds' not in kwargs and ec.lower() == "none":
kwargs['antialiaseds'] = False
if shading == 'gouraud':
if facecolors is not None:
raise ValueError(
"shading='gouraud' can only be used when the colors "
"are specified at the points, not at the faces.")
collection = TriMesh(tri, alpha=alpha, array=point_colors,
cmap=cmap, norm=norm, **kwargs)
else: # 'flat'
# Vertices of triangles.
maskedTris = tri.get_masked_triangles()
verts = np.stack((tri.x[maskedTris], tri.y[maskedTris]), axis=-1)
# Color values.
if facecolors is None:
# One color per triangle, the mean of the 3 vertex color values.
colors = point_colors[maskedTris].mean(axis=1)
elif tri.mask is not None:
# Remove color values of masked triangles.
colors = facecolors[~tri.mask]
else:
colors = facecolors
collection = PolyCollection(verts, alpha=alpha, array=colors,
cmap=cmap, norm=norm, **kwargs)
collection._scale_norm(norm, vmin, vmax)
ax.grid(False)
minx = tri.x.min()
maxx = tri.x.max()
miny = tri.y.min()
maxy = tri.y.max()
corners = (minx, miny), (maxx, maxy)
ax.update_datalim(corners)
ax.autoscale_view()
ax.add_collection(collection)
return collection

View File

@ -0,0 +1,71 @@
from matplotlib.axes import Axes
from matplotlib.collections import PolyCollection, TriMesh
from matplotlib.colors import Normalize, Colormap
from matplotlib.tri._triangulation import Triangulation
from numpy.typing import ArrayLike
from typing import overload, Literal
@overload
def tripcolor(
ax: Axes,
triangulation: Triangulation,
c: ArrayLike = ...,
*,
alpha: float = ...,
norm: str | Normalize | None = ...,
cmap: str | Colormap | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
shading: Literal["flat"] = ...,
facecolors: ArrayLike | None = ...,
**kwargs
) -> PolyCollection: ...
@overload
def tripcolor(
ax: Axes,
x: ArrayLike,
y: ArrayLike,
c: ArrayLike = ...,
*,
alpha: float = ...,
norm: str | Normalize | None = ...,
cmap: str | Colormap | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
shading: Literal["flat"] = ...,
facecolors: ArrayLike | None = ...,
**kwargs
) -> PolyCollection: ...
@overload
def tripcolor(
ax: Axes,
triangulation: Triangulation,
c: ArrayLike = ...,
*,
alpha: float = ...,
norm: str | Normalize | None = ...,
cmap: str | Colormap | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
shading: Literal["gouraud"],
facecolors: ArrayLike | None = ...,
**kwargs
) -> TriMesh: ...
@overload
def tripcolor(
ax: Axes,
x: ArrayLike,
y: ArrayLike,
c: ArrayLike = ...,
*,
alpha: float = ...,
norm: str | Normalize | None = ...,
cmap: str | Colormap | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
shading: Literal["gouraud"],
facecolors: ArrayLike | None = ...,
**kwargs
) -> TriMesh: ...

View File

@ -0,0 +1,86 @@
import numpy as np
from matplotlib.tri._triangulation import Triangulation
import matplotlib.cbook as cbook
import matplotlib.lines as mlines
def triplot(ax, *args, **kwargs):
"""
Draw an unstructured triangular grid as lines and/or markers.
Call signatures::
triplot(triangulation, ...)
triplot(x, y, [triangles], *, [mask=mask], ...)
The triangular grid can be specified either by passing a `.Triangulation`
object as the first parameter, or by passing the points *x*, *y* and
optionally the *triangles* and a *mask*. If neither of *triangulation* or
*triangles* are given, the triangulation is calculated on the fly.
Parameters
----------
triangulation : `.Triangulation`
An already created triangular grid.
x, y, triangles, mask
Parameters defining the triangular grid. See `.Triangulation`.
This is mutually exclusive with specifying *triangulation*.
other_parameters
All other args and kwargs are forwarded to `~.Axes.plot`.
Returns
-------
lines : `~matplotlib.lines.Line2D`
The drawn triangles edges.
markers : `~matplotlib.lines.Line2D`
The drawn marker nodes.
"""
import matplotlib.axes
tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, **kwargs)
x, y, edges = (tri.x, tri.y, tri.edges)
# Decode plot format string, e.g., 'ro-'
fmt = args[0] if args else ""
linestyle, marker, color = matplotlib.axes._base._process_plot_format(fmt)
# Insert plot format string into a copy of kwargs (kwargs values prevail).
kw = cbook.normalize_kwargs(kwargs, mlines.Line2D)
for key, val in zip(('linestyle', 'marker', 'color'),
(linestyle, marker, color)):
if val is not None:
kw.setdefault(key, val)
# Draw lines without markers.
# Note 1: If we drew markers here, most markers would be drawn more than
# once as they belong to several edges.
# Note 2: We insert nan values in the flattened edges arrays rather than
# plotting directly (triang.x[edges].T, triang.y[edges].T)
# as it considerably speeds-up code execution.
linestyle = kw['linestyle']
kw_lines = {
**kw,
'marker': 'None', # No marker to draw.
'zorder': kw.get('zorder', 1), # Path default zorder is used.
}
if linestyle not in [None, 'None', '', ' ']:
tri_lines_x = np.insert(x[edges], 2, np.nan, axis=1)
tri_lines_y = np.insert(y[edges], 2, np.nan, axis=1)
tri_lines = ax.plot(tri_lines_x.ravel(), tri_lines_y.ravel(),
**kw_lines)
else:
tri_lines = ax.plot([], [], **kw_lines)
# Draw markers separately.
marker = kw['marker']
kw_markers = {
**kw,
'linestyle': 'None', # No line to draw.
}
kw_markers.pop('label', None)
if marker not in [None, 'None', '', ' ']:
tri_markers = ax.plot(x, y, **kw_markers)
else:
tri_markers = ax.plot([], [], **kw_markers)
return tri_lines + tri_markers

View File

@ -0,0 +1,15 @@
from matplotlib.tri._triangulation import Triangulation
from matplotlib.axes import Axes
from matplotlib.lines import Line2D
from typing import overload
from numpy.typing import ArrayLike
@overload
def triplot(
ax: Axes, triangulation: Triangulation, *args, **kwargs
) -> tuple[Line2D, Line2D]: ...
@overload
def triplot(
ax: Axes, x: ArrayLike, y: ArrayLike, triangles: ArrayLike = ..., *args, **kwargs
) -> tuple[Line2D, Line2D]: ...

View File

@ -0,0 +1,307 @@
"""
Mesh refinement for triangular grids.
"""
import numpy as np
from matplotlib import _api
from matplotlib.tri._triangulation import Triangulation
import matplotlib.tri._triinterpolate
class TriRefiner:
"""
Abstract base class for classes implementing mesh refinement.
A TriRefiner encapsulates a Triangulation object and provides tools for
mesh refinement and interpolation.
Derived classes must implement:
- ``refine_triangulation(return_tri_index=False, **kwargs)`` , where
the optional keyword arguments *kwargs* are defined in each
TriRefiner concrete implementation, and which returns:
- a refined triangulation,
- optionally (depending on *return_tri_index*), for each
point of the refined triangulation: the index of
the initial triangulation triangle to which it belongs.
- ``refine_field(z, triinterpolator=None, **kwargs)``, where:
- *z* array of field values (to refine) defined at the base
triangulation nodes,
- *triinterpolator* is an optional `~matplotlib.tri.TriInterpolator`,
- the other optional keyword arguments *kwargs* are defined in
each TriRefiner concrete implementation;
and which returns (as a tuple) a refined triangular mesh and the
interpolated values of the field at the refined triangulation nodes.
"""
def __init__(self, triangulation):
_api.check_isinstance(Triangulation, triangulation=triangulation)
self._triangulation = triangulation
class UniformTriRefiner(TriRefiner):
"""
Uniform mesh refinement by recursive subdivisions.
Parameters
----------
triangulation : `~matplotlib.tri.Triangulation`
The encapsulated triangulation (to be refined)
"""
# See Also
# --------
# :class:`~matplotlib.tri.CubicTriInterpolator` and
# :class:`~matplotlib.tri.TriAnalyzer`.
# """
def __init__(self, triangulation):
super().__init__(triangulation)
def refine_triangulation(self, return_tri_index=False, subdiv=3):
"""
Compute a uniformly refined triangulation *refi_triangulation* of
the encapsulated :attr:`triangulation`.
This function refines the encapsulated triangulation by splitting each
father triangle into 4 child sub-triangles built on the edges midside
nodes, recursing *subdiv* times. In the end, each triangle is hence
divided into ``4**subdiv`` child triangles.
Parameters
----------
return_tri_index : bool, default: False
Whether an index table indicating the father triangle index of each
point is returned.
subdiv : int, default: 3
Recursion level for the subdivision.
Each triangle is divided into ``4**subdiv`` child triangles;
hence, the default results in 64 refined subtriangles for each
triangle of the initial triangulation.
Returns
-------
refi_triangulation : `~matplotlib.tri.Triangulation`
The refined triangulation.
found_index : int array
Index of the initial triangulation containing triangle, for each
point of *refi_triangulation*.
Returned only if *return_tri_index* is set to True.
"""
refi_triangulation = self._triangulation
ntri = refi_triangulation.triangles.shape[0]
# Computes the triangulation ancestors numbers in the reference
# triangulation.
ancestors = np.arange(ntri, dtype=np.int32)
for _ in range(subdiv):
refi_triangulation, ancestors = self._refine_triangulation_once(
refi_triangulation, ancestors)
refi_npts = refi_triangulation.x.shape[0]
refi_triangles = refi_triangulation.triangles
# Now we compute found_index table if needed
if return_tri_index:
# We have to initialize found_index with -1 because some nodes
# may very well belong to no triangle at all, e.g., in case of
# Delaunay Triangulation with DuplicatePointWarning.
found_index = np.full(refi_npts, -1, dtype=np.int32)
tri_mask = self._triangulation.mask
if tri_mask is None:
found_index[refi_triangles] = np.repeat(ancestors,
3).reshape(-1, 3)
else:
# There is a subtlety here: we want to avoid whenever possible
# that refined points container is a masked triangle (which
# would result in artifacts in plots).
# So we impose the numbering from masked ancestors first,
# then overwrite it with unmasked ancestor numbers.
ancestor_mask = tri_mask[ancestors]
found_index[refi_triangles[ancestor_mask, :]
] = np.repeat(ancestors[ancestor_mask],
3).reshape(-1, 3)
found_index[refi_triangles[~ancestor_mask, :]
] = np.repeat(ancestors[~ancestor_mask],
3).reshape(-1, 3)
return refi_triangulation, found_index
else:
return refi_triangulation
def refine_field(self, z, triinterpolator=None, subdiv=3):
"""
Refine a field defined on the encapsulated triangulation.
Parameters
----------
z : (npoints,) array-like
Values of the field to refine, defined at the nodes of the
encapsulated triangulation. (``n_points`` is the number of points
in the initial triangulation)
triinterpolator : `~matplotlib.tri.TriInterpolator`, optional
Interpolator used for field interpolation. If not specified,
a `~matplotlib.tri.CubicTriInterpolator` will be used.
subdiv : int, default: 3
Recursion level for the subdivision.
Each triangle is divided into ``4**subdiv`` child triangles.
Returns
-------
refi_tri : `~matplotlib.tri.Triangulation`
The returned refined triangulation.
refi_z : 1D array of length: *refi_tri* node count.
The returned interpolated field (at *refi_tri* nodes).
"""
if triinterpolator is None:
interp = matplotlib.tri.CubicTriInterpolator(
self._triangulation, z)
else:
_api.check_isinstance(matplotlib.tri.TriInterpolator,
triinterpolator=triinterpolator)
interp = triinterpolator
refi_tri, found_index = self.refine_triangulation(
subdiv=subdiv, return_tri_index=True)
refi_z = interp._interpolate_multikeys(
refi_tri.x, refi_tri.y, tri_index=found_index)[0]
return refi_tri, refi_z
@staticmethod
def _refine_triangulation_once(triangulation, ancestors=None):
"""
Refine a `.Triangulation` by splitting each triangle into 4
child-masked_triangles built on the edges midside nodes.
Masked triangles, if present, are also split, but their children
returned masked.
If *ancestors* is not provided, returns only a new triangulation:
child_triangulation.
If the array-like key table *ancestor* is given, it shall be of shape
(ntri,) where ntri is the number of *triangulation* masked_triangles.
In this case, the function returns
(child_triangulation, child_ancestors)
child_ancestors is defined so that the 4 child masked_triangles share
the same index as their father: child_ancestors.shape = (4 * ntri,).
"""
x = triangulation.x
y = triangulation.y
# According to tri.triangulation doc:
# neighbors[i, j] is the triangle that is the neighbor
# to the edge from point index masked_triangles[i, j] to point
# index masked_triangles[i, (j+1)%3].
neighbors = triangulation.neighbors
triangles = triangulation.triangles
npts = np.shape(x)[0]
ntri = np.shape(triangles)[0]
if ancestors is not None:
ancestors = np.asarray(ancestors)
if np.shape(ancestors) != (ntri,):
raise ValueError(
"Incompatible shapes provide for "
"triangulation.masked_triangles and ancestors: "
f"{np.shape(triangles)} and {np.shape(ancestors)}")
# Initiating tables refi_x and refi_y of the refined triangulation
# points
# hint: each apex is shared by 2 masked_triangles except the borders.
borders = np.sum(neighbors == -1)
added_pts = (3*ntri + borders) // 2
refi_npts = npts + added_pts
refi_x = np.zeros(refi_npts)
refi_y = np.zeros(refi_npts)
# First part of refi_x, refi_y is just the initial points
refi_x[:npts] = x
refi_y[:npts] = y
# Second part contains the edge midside nodes.
# Each edge belongs to 1 triangle (if border edge) or is shared by 2
# masked_triangles (interior edge).
# We first build 2 * ntri arrays of edge starting nodes (edge_elems,
# edge_apexes); we then extract only the masters to avoid overlaps.
# The so-called 'master' is the triangle with biggest index
# The 'slave' is the triangle with lower index
# (can be -1 if border edge)
# For slave and master we will identify the apex pointing to the edge
# start
edge_elems = np.tile(np.arange(ntri, dtype=np.int32), 3)
edge_apexes = np.repeat(np.arange(3, dtype=np.int32), ntri)
edge_neighbors = neighbors[edge_elems, edge_apexes]
mask_masters = (edge_elems > edge_neighbors)
# Identifying the "masters" and adding to refi_x, refi_y vec
masters = edge_elems[mask_masters]
apex_masters = edge_apexes[mask_masters]
x_add = (x[triangles[masters, apex_masters]] +
x[triangles[masters, (apex_masters+1) % 3]]) * 0.5
y_add = (y[triangles[masters, apex_masters]] +
y[triangles[masters, (apex_masters+1) % 3]]) * 0.5
refi_x[npts:] = x_add
refi_y[npts:] = y_add
# Building the new masked_triangles; each old masked_triangles hosts
# 4 new masked_triangles
# there are 6 pts to identify per 'old' triangle, 3 new_pt_corner and
# 3 new_pt_midside
new_pt_corner = triangles
# What is the index in refi_x, refi_y of point at middle of apex iapex
# of elem ielem ?
# If ielem is the apex master: simple count, given the way refi_x was
# built.
# If ielem is the apex slave: yet we do not know; but we will soon
# using the neighbors table.
new_pt_midside = np.empty([ntri, 3], dtype=np.int32)
cum_sum = npts
for imid in range(3):
mask_st_loc = (imid == apex_masters)
n_masters_loc = np.sum(mask_st_loc)
elem_masters_loc = masters[mask_st_loc]
new_pt_midside[:, imid][elem_masters_loc] = np.arange(
n_masters_loc, dtype=np.int32) + cum_sum
cum_sum += n_masters_loc
# Now dealing with slave elems.
# for each slave element we identify the master and then the inode
# once slave_masters is identified, slave_masters_apex is such that:
# neighbors[slaves_masters, slave_masters_apex] == slaves
mask_slaves = np.logical_not(mask_masters)
slaves = edge_elems[mask_slaves]
slaves_masters = edge_neighbors[mask_slaves]
diff_table = np.abs(neighbors[slaves_masters, :] -
np.outer(slaves, np.ones(3, dtype=np.int32)))
slave_masters_apex = np.argmin(diff_table, axis=1)
slaves_apex = edge_apexes[mask_slaves]
new_pt_midside[slaves, slaves_apex] = new_pt_midside[
slaves_masters, slave_masters_apex]
# Builds the 4 child masked_triangles
child_triangles = np.empty([ntri*4, 3], dtype=np.int32)
child_triangles[0::4, :] = np.vstack([
new_pt_corner[:, 0], new_pt_midside[:, 0],
new_pt_midside[:, 2]]).T
child_triangles[1::4, :] = np.vstack([
new_pt_corner[:, 1], new_pt_midside[:, 1],
new_pt_midside[:, 0]]).T
child_triangles[2::4, :] = np.vstack([
new_pt_corner[:, 2], new_pt_midside[:, 2],
new_pt_midside[:, 1]]).T
child_triangles[3::4, :] = np.vstack([
new_pt_midside[:, 0], new_pt_midside[:, 1],
new_pt_midside[:, 2]]).T
child_triangulation = Triangulation(refi_x, refi_y, child_triangles)
# Builds the child mask
if triangulation.mask is not None:
child_triangulation.set_mask(np.repeat(triangulation.mask, 4))
if ancestors is None:
return child_triangulation
else:
return child_triangulation, np.repeat(ancestors, 4)

View File

@ -0,0 +1,31 @@
from typing import Literal, overload
import numpy as np
from numpy.typing import ArrayLike
from matplotlib.tri._triangulation import Triangulation
from matplotlib.tri._triinterpolate import TriInterpolator
class TriRefiner:
def __init__(self, triangulation: Triangulation) -> None: ...
class UniformTriRefiner(TriRefiner):
def __init__(self, triangulation: Triangulation) -> None: ...
@overload
def refine_triangulation(
self, *, return_tri_index: Literal[True], subdiv: int = ...
) -> tuple[Triangulation, np.ndarray]: ...
@overload
def refine_triangulation(
self, return_tri_index: Literal[False] = ..., subdiv: int = ...
) -> Triangulation: ...
@overload
def refine_triangulation(
self, return_tri_index: bool = ..., subdiv: int = ...
) -> tuple[Triangulation, np.ndarray] | Triangulation: ...
def refine_field(
self,
z: ArrayLike,
triinterpolator: TriInterpolator | None = ...,
subdiv: int = ...,
) -> tuple[Triangulation, np.ndarray]: ...

View File

@ -0,0 +1,263 @@
"""
Tools for triangular grids.
"""
import numpy as np
from matplotlib import _api
from matplotlib.tri import Triangulation
class TriAnalyzer:
"""
Define basic tools for triangular mesh analysis and improvement.
A TriAnalyzer encapsulates a `.Triangulation` object and provides basic
tools for mesh analysis and mesh improvement.
Attributes
----------
scale_factors
Parameters
----------
triangulation : `~matplotlib.tri.Triangulation`
The encapsulated triangulation to analyze.
"""
def __init__(self, triangulation):
_api.check_isinstance(Triangulation, triangulation=triangulation)
self._triangulation = triangulation
@property
def scale_factors(self):
"""
Factors to rescale the triangulation into a unit square.
Returns
-------
(float, float)
Scaling factors (kx, ky) so that the triangulation
``[triangulation.x * kx, triangulation.y * ky]``
fits exactly inside a unit square.
"""
compressed_triangles = self._triangulation.get_masked_triangles()
node_used = (np.bincount(np.ravel(compressed_triangles),
minlength=self._triangulation.x.size) != 0)
return (1 / np.ptp(self._triangulation.x[node_used]),
1 / np.ptp(self._triangulation.y[node_used]))
def circle_ratios(self, rescale=True):
"""
Return a measure of the triangulation triangles flatness.
The ratio of the incircle radius over the circumcircle radius is a
widely used indicator of a triangle flatness.
It is always ``<= 0.5`` and ``== 0.5`` only for equilateral
triangles. Circle ratios below 0.01 denote very flat triangles.
To avoid unduly low values due to a difference of scale between the 2
axis, the triangular mesh can first be rescaled to fit inside a unit
square with `scale_factors` (Only if *rescale* is True, which is
its default value).
Parameters
----------
rescale : bool, default: True
If True, internally rescale (based on `scale_factors`), so that the
(unmasked) triangles fit exactly inside a unit square mesh.
Returns
-------
masked array
Ratio of the incircle radius over the circumcircle radius, for
each 'rescaled' triangle of the encapsulated triangulation.
Values corresponding to masked triangles are masked out.
"""
# Coords rescaling
if rescale:
(kx, ky) = self.scale_factors
else:
(kx, ky) = (1.0, 1.0)
pts = np.vstack([self._triangulation.x*kx,
self._triangulation.y*ky]).T
tri_pts = pts[self._triangulation.triangles]
# Computes the 3 side lengths
a = tri_pts[:, 1, :] - tri_pts[:, 0, :]
b = tri_pts[:, 2, :] - tri_pts[:, 1, :]
c = tri_pts[:, 0, :] - tri_pts[:, 2, :]
a = np.hypot(a[:, 0], a[:, 1])
b = np.hypot(b[:, 0], b[:, 1])
c = np.hypot(c[:, 0], c[:, 1])
# circumcircle and incircle radii
s = (a+b+c)*0.5
prod = s*(a+b-s)*(a+c-s)*(b+c-s)
# We have to deal with flat triangles with infinite circum_radius
bool_flat = (prod == 0.)
if np.any(bool_flat):
# Pathologic flow
ntri = tri_pts.shape[0]
circum_radius = np.empty(ntri, dtype=np.float64)
circum_radius[bool_flat] = np.inf
abc = a*b*c
circum_radius[~bool_flat] = abc[~bool_flat] / (
4.0*np.sqrt(prod[~bool_flat]))
else:
# Normal optimized flow
circum_radius = (a*b*c) / (4.0*np.sqrt(prod))
in_radius = (a*b*c) / (4.0*circum_radius*s)
circle_ratio = in_radius/circum_radius
mask = self._triangulation.mask
if mask is None:
return circle_ratio
else:
return np.ma.array(circle_ratio, mask=mask)
def get_flat_tri_mask(self, min_circle_ratio=0.01, rescale=True):
"""
Eliminate excessively flat border triangles from the triangulation.
Returns a mask *new_mask* which allows to clean the encapsulated
triangulation from its border-located flat triangles
(according to their :meth:`circle_ratios`).
This mask is meant to be subsequently applied to the triangulation
using `.Triangulation.set_mask`.
*new_mask* is an extension of the initial triangulation mask
in the sense that an initially masked triangle will remain masked.
The *new_mask* array is computed recursively; at each step flat
triangles are removed only if they share a side with the current mesh
border. Thus, no new holes in the triangulated domain will be created.
Parameters
----------
min_circle_ratio : float, default: 0.01
Border triangles with incircle/circumcircle radii ratio r/R will
be removed if r/R < *min_circle_ratio*.
rescale : bool, default: True
If True, first, internally rescale (based on `scale_factors`) so
that the (unmasked) triangles fit exactly inside a unit square
mesh. This rescaling accounts for the difference of scale which
might exist between the 2 axis.
Returns
-------
array of bool
Mask to apply to encapsulated triangulation.
All the initially masked triangles remain masked in the
*new_mask*.
Notes
-----
The rationale behind this function is that a Delaunay
triangulation - of an unstructured set of points - sometimes contains
almost flat triangles at its border, leading to artifacts in plots
(especially for high-resolution contouring).
Masked with computed *new_mask*, the encapsulated
triangulation would contain no more unmasked border triangles
with a circle ratio below *min_circle_ratio*, thus improving the
mesh quality for subsequent plots or interpolation.
"""
# Recursively computes the mask_current_borders, true if a triangle is
# at the border of the mesh OR touching the border through a chain of
# invalid aspect ratio masked_triangles.
ntri = self._triangulation.triangles.shape[0]
mask_bad_ratio = self.circle_ratios(rescale) < min_circle_ratio
current_mask = self._triangulation.mask
if current_mask is None:
current_mask = np.zeros(ntri, dtype=bool)
valid_neighbors = np.copy(self._triangulation.neighbors)
renum_neighbors = np.arange(ntri, dtype=np.int32)
nadd = -1
while nadd != 0:
# The active wavefront is the triangles from the border (unmasked
# but with a least 1 neighbor equal to -1
wavefront = (np.min(valid_neighbors, axis=1) == -1) & ~current_mask
# The element from the active wavefront will be masked if their
# circle ratio is bad.
added_mask = wavefront & mask_bad_ratio
current_mask = added_mask | current_mask
nadd = np.sum(added_mask)
# now we have to update the tables valid_neighbors
valid_neighbors[added_mask, :] = -1
renum_neighbors[added_mask] = -1
valid_neighbors = np.where(valid_neighbors == -1, -1,
renum_neighbors[valid_neighbors])
return np.ma.filled(current_mask, True)
def _get_compressed_triangulation(self):
"""
Compress (if masked) the encapsulated triangulation.
Returns minimal-length triangles array (*compressed_triangles*) and
coordinates arrays (*compressed_x*, *compressed_y*) that can still
describe the unmasked triangles of the encapsulated triangulation.
Returns
-------
compressed_triangles : array-like
the returned compressed triangulation triangles
compressed_x : array-like
the returned compressed triangulation 1st coordinate
compressed_y : array-like
the returned compressed triangulation 2nd coordinate
tri_renum : int array
renumbering table to translate the triangle numbers from the
encapsulated triangulation into the new (compressed) renumbering.
-1 for masked triangles (deleted from *compressed_triangles*).
node_renum : int array
renumbering table to translate the point numbers from the
encapsulated triangulation into the new (compressed) renumbering.
-1 for unused points (i.e. those deleted from *compressed_x* and
*compressed_y*).
"""
# Valid triangles and renumbering
tri_mask = self._triangulation.mask
compressed_triangles = self._triangulation.get_masked_triangles()
ntri = self._triangulation.triangles.shape[0]
if tri_mask is not None:
tri_renum = self._total_to_compress_renum(~tri_mask)
else:
tri_renum = np.arange(ntri, dtype=np.int32)
# Valid nodes and renumbering
valid_node = (np.bincount(np.ravel(compressed_triangles),
minlength=self._triangulation.x.size) != 0)
compressed_x = self._triangulation.x[valid_node]
compressed_y = self._triangulation.y[valid_node]
node_renum = self._total_to_compress_renum(valid_node)
# Now renumbering the valid triangles nodes
compressed_triangles = node_renum[compressed_triangles]
return (compressed_triangles, compressed_x, compressed_y, tri_renum,
node_renum)
@staticmethod
def _total_to_compress_renum(valid):
"""
Parameters
----------
valid : 1D bool array
Validity mask.
Returns
-------
int array
Array so that (`valid_array` being a compressed array
based on a `masked_array` with mask ~*valid*):
- For all i with valid[i] = True:
valid_array[renum[i]] = masked_array[i]
- For all i with valid[i] = False:
renum[i] = -1 (invalid value)
"""
renum = np.full(np.size(valid), -1, dtype=np.int32)
n_valid = np.sum(valid)
renum[valid] = np.arange(n_valid, dtype=np.int32)
return renum

View File

@ -0,0 +1,12 @@
from matplotlib.tri import Triangulation
import numpy as np
class TriAnalyzer:
def __init__(self, triangulation: Triangulation) -> None: ...
@property
def scale_factors(self) -> tuple[float, float]: ...
def circle_ratios(self, rescale: bool = ...) -> np.ndarray: ...
def get_flat_tri_mask(
self, min_circle_ratio: float = ..., rescale: bool = ...
) -> np.ndarray: ...