Fixed database typo and removed unnecessary class identifier.

This commit is contained in:
Batuhan Berk Başoğlu 2020-10-14 10:10:37 -04:00
parent 00ad49a143
commit 45fb349a7d
5098 changed files with 952558 additions and 85 deletions

View file

@ -0,0 +1,12 @@
"""
Unstructured triangular grid functions.
"""
from .triangulation import *
from .tricontour import *
from .tritools import *
from .trifinder import *
from .triinterpolate import *
from .trirefine import *
from .tripcolor import *
from .triplot import *

View file

@ -0,0 +1,220 @@
import numpy as np
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 : array-like of shape (npoints)
Coordinates of grid points.
triangles : int array-like of shape (ntri, 3), 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 : bool array-like of shape (ntri), optional
Which triangles are masked out.
Attributes
----------
edges : int array of shape (nedges, 2)
See `~.Triangulation.edges`
neighbors : int array of shape (ntri, 3)
See `~.Triangulation.neighbors`
mask : bool array of shape (ntri, 3)
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 1-D arrays")
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)
self.is_delaunay = True
else:
# Triangulation specified. Copy, since we may correct triangle
# orientation.
self.triangles = np.array(triangles, dtype=np.int32, order='C')
if self.triangles.ndim != 2 or self.triangles.shape[1] != 3:
raise ValueError('triangles must be a (?, 3) array')
if self.triangles.max() >= len(self.x):
raise ValueError('triangles max element is out of bounds')
if self.triangles.min() < 0:
raise ValueError('triangles min element is out of bounds')
if mask is not None:
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')
# Underlying C++ object is not created until first needed.
self._cpp_triangulation = None
# Default TriFinder not created until needed.
self._trifinder = None
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 it's 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, self._edges,
self._neighbors, not self.is_delaunay)
return self._cpp_triangulation
def get_masked_triangles(self):
"""
Return an array of triangles that are not masked.
"""
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
else:
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)
triangulation = Triangulation(x, y, triangles, mask)
return triangulation, 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)
# 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,321 @@
import numpy as np
from matplotlib import docstring
from matplotlib.contour import ContourSet
from matplotlib.tri.triangulation import Triangulation
class TriContourSet(ContourSet):
"""
Create and store a set of contour lines or filled regions for
a triangular grid.
User-callable method: clabel
Attributes
----------
ax
The axes object in which the contours are drawn.
collections
A silent_list of LineCollections or PolyCollections.
levels
Contour levels.
layers
Same as levels for line contours; half-way between
levels for filled contours. See :meth:`_process_colors`.
"""
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
object. The remaining arguments and keyword arguments
are described in the docstring of `~.Axes.tricontour`.
"""
ContourSet.__init__(self, ax, *args, **kwargs)
def _process_args(self, *args, **kwargs):
"""
Process args and kwargs.
"""
if isinstance(args[0], TriContourSet):
C = args[0].cppContourGenerator
if self.levels is None:
self.levels = args[0].levels
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.cppContourGenerator = C
return kwargs
def _get_allsegs_and_allkinds(self):
"""
Create and return allsegs and allkinds by calling underlying C code.
"""
allsegs = []
if self.filled:
lowers, uppers = self._get_lowers_and_uppers()
allkinds = []
for lower, upper in zip(lowers, uppers):
segs, kinds = self.cppContourGenerator.create_filled_contour(
lower, upper)
allsegs.append([segs])
allkinds.append([kinds])
else:
allkinds = None
for level in self.levels:
segs = self.cppContourGenerator.create_contour(level)
allsegs.append(segs)
return allsegs, allkinds
def _contour_args(self, args, kwargs):
if self.filled:
fn = 'contourf'
else:
fn = 'contour'
tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args,
**kwargs)
z = np.ma.asarray(args[0])
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:
raise ValueError('Cannot %s log of negative values.' % fn)
self._process_contour_level_args(args[1:])
return (tri, z)
docstring.interpd.update(_tricontour_doc="""
Draw contour %(type)s on an unstructured triangular grid.
The triangulation can be specified in one of two ways; either ::
%(func)s(triangulation, ...)
where *triangulation* is a `.Triangulation` object, or ::
%(func)s(x, y, ...)
%(func)s(x, y, triangles, ...)
%(func)s(x, y, triangles=triangles, ...)
%(func)s(x, y, mask=mask, ...)
%(func)s(x, y, triangles, mask=mask, ...)
in which case a `.Triangulation` object will be created. See that class'
docstring for an explanation of these cases.
The remaining arguments may be::
%(func)s(..., Z)
where *Z* is the array of values to contour, one per point in the
triangulation. The level values are chosen automatically.
::
%(func)s(..., Z, levels)
contour up to *levels+1* automatically chosen contour levels (*levels*
intervals).
::
%(func)s(..., Z, levels)
draw contour %(type)s at the values specified in sequence *levels*, which must
be in increasing order.
::
%(func)s(Z, **kwargs)
Use keyword arguments to control colors, linewidth, origin, cmap ... see below
for more details.
Parameters
----------
triangulation : `.Triangulation`, optional
The unstructured triangular grid.
If specified, then *x*, *y*, *triangles*, and *mask* are not accepted.
x, y : array-like, optional
The coordinates of the values in *Z*.
triangles : int array-like of shape (ntri, 3), 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 : bool array-like of shape (ntri), optional
Which triangles are masked out.
Z : array-like(N, M)
The height values over which the contour is drawn.
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
*vmin* and *vmax*.
If array-like, draw contour lines at the specified levels. The values must
be in increasing order.
Returns
-------
`~matplotlib.tri.TriContourSet`
Other Parameters
----------------
colors : color string or sequence of colors, 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's 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 : str or `.Colormap`, default: :rc:`image.cmap`
A `.Colormap` instance or registered colormap name. The colormap maps the
level values to colors.
If both *colors* and *cmap* are given, an error is raised.
norm : `~matplotlib.colors.Normalize`, optional
If a colormap is used, the `.Normalize` instance scales the level values to
the canonical colormap range [0, 1] for mapping to colors. If not given,
the default linear scaling is used.
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`.""")
@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
antialiased : bool, default: True
Whether to use antialiasing.
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,93 @@
import numpy as np
from matplotlib import cbook
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):
cbook._check_isinstance(Triangulation, triangulation=triangulation)
self._triangulation = triangulation
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
TriFinder.__init__(self, 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()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,131 @@
import numpy as np
from matplotlib import cbook
from matplotlib.collections import PolyCollection, TriMesh
from matplotlib.colors import Normalize
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.
The triangulation can be specified in one of two ways; either::
tripcolor(triangulation, ...)
where triangulation is a `.Triangulation` object, or
::
tripcolor(x, y, ...)
tripcolor(x, y, triangles, ...)
tripcolor(x, y, triangles=triangles, ...)
tripcolor(x, y, mask=mask, ...)
tripcolor(x, y, triangles, mask=mask, ...)
in which case a Triangulation object will be created. See `.Triangulation`
for a explanation of these possibilities.
The next argument must be *C*, the array of color values, either
one per point in the triangulation if color values are defined at
points, or one per triangle in the triangulation if color values
are defined at 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 kwarg ``facecolors=C`` instead of just ``C``.
*shading* may be 'flat' (the default) or 'gouraud'. If *shading*
is 'flat' and C values 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.
The remaining kwargs are the same as for `~.Axes.pcolor`.
"""
cbook._check_in_list(['flat', 'gouraud'], shading=shading)
tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, **kwargs)
# C is the colors array defined at either points or faces (i.e. triangles).
# If facecolors is None, C are defined at points.
# If facecolors is not None, C are defined at faces.
if facecolors is not None:
C = facecolors
else:
C = np.asarray(args[0])
# If there are a different number of points and triangles in the
# triangulation, can omit facecolors kwarg as it is obvious from
# length of C whether it refers to points or faces.
# Do not do this for gouraud shading.
if (facecolors is None and len(C) == len(tri.triangles) and
len(C) != len(tri.x) and shading != 'gouraud'):
facecolors = C
# Check length of C is OK.
if ((facecolors is None and len(C) != len(tri.x)) or
(facecolors is not None and len(C) != len(tri.triangles))):
raise ValueError('Length of color values array must be the same '
'as either the number of triangulation points '
'or 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('Gouraud shading does not support the use '
'of facecolors kwarg')
if len(C) != len(tri.x):
raise ValueError('For gouraud shading, the length of color '
'values array must be the same as the '
'number of triangulation points')
collection = TriMesh(tri, **kwargs)
else:
# 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.
C = C[maskedTris].mean(axis=1)
elif tri.mask is not None:
# Remove color values of masked triangles.
C = C[~tri.mask]
collection = PolyCollection(verts, **kwargs)
collection.set_alpha(alpha)
collection.set_array(C)
cbook._check_isinstance((Normalize, None), norm=norm)
collection.set_cmap(cmap)
collection.set_norm(norm)
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,82 @@
import numpy as np
from matplotlib.tri.triangulation import Triangulation
def triplot(ax, *args, **kwargs):
"""
Draw a unstructured triangular grid as lines and/or markers.
The triangulation to plot can be specified in one of two ways; either::
triplot(triangulation, ...)
where triangulation is a `.Triangulation` object, or
::
triplot(x, y, ...)
triplot(x, y, triangles, ...)
triplot(x, y, triangles=triangles, ...)
triplot(x, y, mask=mask, ...)
triplot(x, y, triangles, mask=mask, ...)
in which case a Triangulation object will be created. See `.Triangulation`
for a explanation of these possibilities.
The remaining args and kwargs are the same as for `~.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 = kwargs.copy()
for key, val in zip(('linestyle', 'marker', 'color'),
(linestyle, marker, color)):
if val is not None:
kw[key] = kwargs.get(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.
}
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,307 @@
"""
Mesh refinement for triangular grids.
"""
import numpy as np
from matplotlib import cbook
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):
cbook._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):
TriRefiner.__init__(self, triangulation)
def refine_triangulation(self, return_tri_index=False, subdiv=3):
"""
Compute an 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 : 1d-array-like of length ``n_points``
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:
cbook._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: {0} and {1}".format(
np.shape(triangles), 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,263 @@
"""
Tools for triangular grids.
"""
import numpy as np
from matplotlib import cbook
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):
cbook._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
-------
bool array-like
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