Skip to content

Commit 413e18e

Browse files
committed
Added LinearTriInterpolator class.
1 parent b903eec commit 413e18e

File tree

10 files changed

+303
-2
lines changed

10 files changed

+303
-2
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
2012-12-22 Added classes for interpolation within triangular grids
2+
(LinearTriInterpolator) and to find the triangles in which points
3+
lie (TrapezoidMapTriFinder) to matplotlib.tri module. - IMT
4+
15
2012-12-05 Added MatplotlibDeprecationWarning class for signaling deprecation.
26
Matplotlib developers can use this class as follows:
37

doc/api/tri_api.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ triangular grids
1313

1414
.. autoclass:: matplotlib.tri.TrapezoidMapTriFinder
1515
:members: __call__
16+
17+
.. autoclass:: matplotlib.tri.TriInterpolator
18+
:members:
19+
20+
.. autoclass:: matplotlib.tri.LinearTriInterpolator
21+
:members: __call__

doc/users/whats_new.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ using Inkscape for example, while preserving their intended position. For
4949
`svg` please note that you'll have to disable the default text-to-path
5050
conversion (`mpl.rc('svg', fonttype='none')`).
5151

52+
Triangular grid interpolation
53+
-----------------------------
54+
Ian Thomas added classes to perform interpolation within triangular grids
55+
(:class:`~matplotlib.tri.LinearTriInterpolator`) and a utility class to find
56+
the triangles in which points lie (
57+
:class:`~matplotlib.tri.TrapezoidMapTriFinder`).
58+
5259
.. _whats-new-1-2:
5360

5461
new in matplotlib-1.2
@@ -298,7 +305,7 @@ to address the most common layout issues.
298305
fig, axes_list = plt.subplots(2, 1)
299306
for ax in axes_list.flat:
300307
ax.set(xlabel="x-label", ylabel="y-label", title="before tight_layout")
301-
ax.locator_params(nbins=3)
308+
ax.locator_params(nbins=3)
302309

303310
plt.show()
304311

@@ -308,7 +315,7 @@ to address the most common layout issues.
308315
fig, axes_list = plt.subplots(2, 1)
309316
for ax in axes_list.flat:
310317
ax.set(xlabel="x-label", ylabel="y-label", title="after tight_layout")
311-
ax.locator_params(nbins=3)
318+
ax.locator_params(nbins=3)
312319

313320
plt.tight_layout()
314321
plt.show()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
Interpolation from triangular grid to quad grid.
3+
"""
4+
import matplotlib.pyplot as plt
5+
import matplotlib.tri as mtri
6+
import numpy as np
7+
import math
8+
9+
10+
# Create triangulation.
11+
x = np.asarray([0, 1, 2, 3, 0.5, 1.5, 2.5, 1, 2, 1.5])
12+
y = np.asarray([0, 0, 0, 0, 1, 1, 1, 2, 2, 3])
13+
triangles = [[0,1,4], [1,2,5], [2,3,6], [1,5,4], [2,6,5], [4,5,7], [5,6,8],
14+
[5,8,7], [7,8,9]]
15+
triang = mtri.Triangulation(x, y, triangles)
16+
17+
# Interpolate to regularly-spaced quad grid.
18+
z = np.cos(1.5*x)*np.cos(1.5*y)
19+
interp = mtri.LinearTriInterpolator(triang, z)
20+
xi, yi = np.meshgrid(np.linspace(0, 3, 20), np.linspace(0, 3, 20))
21+
zi = interp(xi, yi)
22+
23+
# Plot the triangulation.
24+
plt.subplot(121)
25+
plt.tricontourf(triang, z)
26+
plt.triplot(triang, 'ko-')
27+
plt.title('Triangular grid')
28+
29+
# Plot interpolation to quad grid.
30+
plt.subplot(122)
31+
plt.contourf(xi, yi, zi)
32+
plt.plot(xi, yi, 'k-', alpha=0.5)
33+
plt.plot(xi.T, yi.T, 'k-', alpha=0.5)
34+
plt.title('Linear interpolation')
35+
36+
plt.show()

lib/matplotlib/tests/test_triangulation.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,34 @@ def test_trifinder():
219219
assert_equal(trifinder, triang.get_trifinder())
220220
tris = trifinder(xs, ys)
221221
assert_array_equal(tris, [-1, -1, 1, -1])
222+
223+
def test_triinterp():
224+
# Test points within triangles of masked triangulation.
225+
x,y = np.meshgrid(np.arange(4), np.arange(4))
226+
x = x.ravel()
227+
y = y.ravel()
228+
z = 1.23*x - 4.79*y
229+
triangles = [[0,1,4], [1,5,4], [1,2,5], [2,6,5], [2,3,6], [3,7,6], [4,5,8],
230+
[5,9,8], [5,6,9], [6,10,9], [6,7,10], [7,11,10], [8,9,12],
231+
[9,13,12], [9,10,13], [10,14,13], [10,11,14], [11,15,14]]
232+
mask = np.zeros(len(triangles))
233+
mask[8:10] = 1
234+
triang = mtri.Triangulation(x, y, triangles, mask)
235+
linear_interp = mtri.LinearTriInterpolator(triang, z)
236+
237+
xs = np.linspace(0.25, 2.75, 6)
238+
ys = [0.25, 0.75, 2.25, 2.75]
239+
xs,ys = np.meshgrid(xs,ys)
240+
xs = xs.ravel()
241+
ys = ys.ravel()
242+
zs = linear_interp(xs, ys)
243+
assert_array_almost_equal(zs, (1.23*xs - 4.79*ys))
244+
245+
# Test points outside triangulation.
246+
xs = [-0.25, 1.25, 1.75, 3.25]
247+
ys = xs
248+
xs, ys = np.meshgrid(xs,ys)
249+
xs = xs.ravel()
250+
ys = ys.ravel()
251+
zs = linear_interp(xs, ys)
252+
assert_array_equal(zs.mask, [True]*16)

lib/matplotlib/tri/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
from triangulation import *
77
from tricontour import *
88
from trifinder import *
9+
from triinterpolate import *
910
from tripcolor import *
1011
from triplot import *

lib/matplotlib/tri/_tri.cpp

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ double XYZ::dot(const XYZ& other) const
137137
return x*other.x + y*other.y + z*other.z;
138138
}
139139

140+
double XYZ::length_squared() const
141+
{
142+
return x*x + y*y + z*z;
143+
}
144+
140145
XYZ XYZ::operator-(const XYZ& other) const
141146
{
142147
return XYZ(x - other.x, y - other.y, z - other.z);
@@ -375,6 +380,95 @@ void Triangulation::calculate_neighbors()
375380
// boundary edges, but the boundaries are calculated separately elsewhere.
376381
}
377382

383+
Py::Object Triangulation::calculate_plane_coefficients(const Py::Tuple &args)
384+
{
385+
_VERBOSE("Triangulation::calculate_plane_coefficients");
386+
args.verify_length(1);
387+
388+
PyArrayObject* z = (PyArrayObject*)PyArray_ContiguousFromObject(
389+
args[0].ptr(), PyArray_DOUBLE, 1, 1);
390+
if (z == 0 || PyArray_DIM(z,0) != PyArray_DIM(_x,0)) {
391+
Py_XDECREF(z);
392+
throw Py::ValueError(
393+
"z array must have same length as triangulation x and y arrays");
394+
}
395+
const double* zs = (const double*)PyArray_DATA(z);
396+
397+
npy_intp dims[2] = {_ntri, 3};
398+
PyArrayObject* planes_array = (PyArrayObject*)PyArray_SimpleNew(
399+
2, dims, PyArray_DOUBLE);
400+
double* planes = (double*)PyArray_DATA(planes_array);
401+
const int* tris = get_triangles_ptr();
402+
const double* xs = (const double*)PyArray_DATA(_x);
403+
const double* ys = (const double*)PyArray_DATA(_y);
404+
for (int tri = 0; tri < _ntri; ++tri)
405+
{
406+
if (is_masked(tri))
407+
{
408+
*planes++ = 0.0;
409+
*planes++ = 0.0;
410+
*planes++ = 0.0;
411+
tris += 3;
412+
}
413+
else
414+
{
415+
// Equation of plane for all points r on plane is r.normal = p
416+
// where normal is vector normal to the plane, and p is a constant.
417+
// Rewrite as r_x*normal_x + r_y*normal_y + r_z*normal_z = p
418+
// and rearrange to give
419+
// r_z = (-normal_x/normal_z)*r_x + (-normal_y/normal_z)*r_y +
420+
// p/normal_z
421+
XYZ point0(xs[*tris], ys[*tris], zs[*tris]);
422+
tris++;
423+
XYZ point1(xs[*tris], ys[*tris], zs[*tris]);
424+
tris++;
425+
XYZ point2(xs[*tris], ys[*tris], zs[*tris]);
426+
tris++;
427+
428+
XYZ normal = (point1 - point0).cross(point2 - point0);
429+
430+
if (normal.z == 0.0)
431+
{
432+
// Normal is in x-y plane which means triangle consists of
433+
// colinear points. Try to do the best we can by taking plane
434+
// through longest side of triangle.
435+
double length_sqr_01 = (point1 - point0).length_squared();
436+
double length_sqr_12 = (point2 - point1).length_squared();
437+
double length_sqr_20 = (point0 - point2).length_squared();
438+
if (length_sqr_01 > length_sqr_12)
439+
{
440+
if (length_sqr_01 > length_sqr_20)
441+
normal = normal.cross(point1 - point0);
442+
else
443+
normal = normal.cross(point0 - point2);
444+
}
445+
else
446+
{
447+
if (length_sqr_12 > length_sqr_20)
448+
normal = normal.cross(point2 - point1);
449+
else
450+
normal = normal.cross(point0 - point2);
451+
}
452+
453+
if (normal.z == 0.0)
454+
{
455+
// The 3 triangle points have identical x and y! The best
456+
// we can do here is take normal = (0,0,1) and for the
457+
// constant p take the mean of the 3 points' z-values.
458+
normal = XYZ(0.0, 0.0, 1.0);
459+
point0.z = (point0.z + point1.z + point2.z) / 3.0;
460+
}
461+
}
462+
463+
*planes++ = -normal.x / normal.z; // x
464+
*planes++ = -normal.y / normal.z; // y
465+
*planes++ = normal.dot(point0) / normal.z; // constant
466+
}
467+
}
468+
469+
return Py::asObject((PyObject*)planes_array);
470+
}
471+
378472
void Triangulation::correct_triangles()
379473
{
380474
int* triangles_ptr = (int*)PyArray_DATA(_triangles);
@@ -506,6 +600,9 @@ void Triangulation::init_type()
506600
behaviors().name("Triangulation");
507601
behaviors().doc("Triangulation");
508602

603+
add_varargs_method("calculate_plane_coefficients",
604+
&Triangulation::calculate_plane_coefficients,
605+
"calculate_plane_coefficients(z)");
509606
add_noargs_method("get_edges", &Triangulation::get_edges,
510607
"get_edges()");
511608
add_noargs_method("get_neighbors", &Triangulation::get_neighbors,

lib/matplotlib/tri/_tri.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ struct XYZ
115115
XYZ(const double& x_, const double& y_, const double& z_);
116116
XYZ cross(const XYZ& other) const;
117117
double dot(const XYZ& other) const;
118+
double length_squared() const;
118119
XYZ operator-(const XYZ& other) const;
119120
friend std::ostream& operator<<(std::ostream& os, const XYZ& xyz);
120121

@@ -189,6 +190,13 @@ class Triangulation : public Py::PythonExtension<Triangulation>
189190

190191
virtual ~Triangulation();
191192

193+
/* Calculate plane equation coefficients for all unmasked triangles from
194+
* the point (x,y) coordinates and point z-array of shape (npoints) passed
195+
* in via the args. Returned array has shape (npoints,3) and allows
196+
* z-value at (x,y) coordinates in triangle tri to be calculated using
197+
* z = array[tri,0]*x + array[tri,1]*y + array[tri,2]. */
198+
Py::Object calculate_plane_coefficients(const Py::Tuple &args);
199+
192200
// Return the boundaries collection, creating it if necessary.
193201
const Boundaries& get_boundaries() const;
194202

lib/matplotlib/tri/triangulation.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ def __init__(self, x, y, triangles=None, mask=None):
8888
# Default TriFinder not created until needed.
8989
self._trifinder = None
9090

91+
def calculate_plane_coefficients(self, z):
92+
"""
93+
Calculate plane equation coefficients for all unmasked triangles from
94+
the point (x,y) coordinates and specified z-array of shape (npoints).
95+
Returned array has shape (npoints,3) and allows z-value at (x,y)
96+
position in triangle tri to be calculated using
97+
z = array[tri,0]*x + array[tri,1]*y + array[tri,2].
98+
"""
99+
return self.get_cpp_triangulation().calculate_plane_coefficients(z)
100+
91101
@property
92102
def edges(self):
93103
if self._edges is None:
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from __future__ import print_function
2+
from matplotlib.tri import Triangulation
3+
from matplotlib.tri.trifinder import TriFinder
4+
import numpy as np
5+
6+
7+
class TriInterpolator(object):
8+
"""
9+
Abstract base class for classes used to perform interpolation on
10+
triangular grids.
11+
12+
Derived classes implement __call__(x,y) where x,y are array_like point
13+
coordinates of the same shape, and that returns a masked array of the same
14+
shape containing the interpolated z-values.
15+
"""
16+
def __init__(self, triangulation, z, trifinder=None):
17+
if not isinstance(triangulation, Triangulation):
18+
raise ValueError('Expected a Triangulation object')
19+
self._triangulation = triangulation
20+
21+
self._z = np.asarray(z)
22+
if self._z.shape != self._triangulation.x.shape:
23+
raise ValueError('z array must have same length as triangulation x'
24+
' and y arrays')
25+
26+
if trifinder is not None and not isinstance(trifinder, TriFinder):
27+
raise ValueError('Expected a TriFinder object')
28+
self._trifinder = trifinder or self._triangulation.get_trifinder()
29+
30+
31+
class LinearTriInterpolator(TriInterpolator):
32+
"""
33+
A LinearTriInterpolator performs linear interpolation on a triangular grid.
34+
35+
Each triangle is represented by a plane so that an interpolated value at
36+
point (x,y) lies on the plane of the triangle containing (x,y).
37+
Interpolated values are therefore continuous across the triangulation, but
38+
their first derivatives are discontinuous at edges between triangles.
39+
"""
40+
def __init__(self, triangulation, z, trifinder=None):
41+
"""
42+
*triangulation*: the :class:`~matplotlib.tri.Triangulation` to
43+
interpolate over.
44+
45+
*z*: array_like of shape (npoints).
46+
Array of values, defined at grid points, to interpolate between.
47+
48+
*trifinder*: optional :class:`~matplotlib.tri.TriFinder` object.
49+
If this is not specified, the Triangulation's default TriFinder will
50+
be used by calling :func:`matplotlib.tri.Triangulation.get_trifinder`.
51+
"""
52+
TriInterpolator.__init__(self, triangulation, z, trifinder)
53+
54+
# Store plane coefficients for fast interpolation calculations.
55+
self._plane_coefficients = \
56+
self._triangulation.calculate_plane_coefficients(self._z)
57+
58+
# Store vectorized interpolation function, so can pass in arbitrarily
59+
# shape arrays of x, y and tri and the _single_interp function is
60+
# called in turn with scalar x, y and tri.
61+
self._multi_interp = np.vectorize(self._single_interp,
62+
otypes=[np.float])
63+
64+
def __call__(self, x, y):
65+
"""
66+
Return a masked array containing linearly interpolated values at the
67+
specified x,y points.
68+
69+
*x*, *y* are array_like x and y coordinates of the same shape and any
70+
number of dimensions.
71+
72+
Returned masked array has the same shape as *x* and *y*; values
73+
corresponding to (x,y) points outside of the triangulation are masked
74+
out.
75+
"""
76+
# Check arguments.
77+
x = np.asarray(x, dtype=np.float64)
78+
y = np.asarray(y, dtype=np.float64)
79+
if x.shape != y.shape:
80+
raise ValueError("x and y must be equal-shaped arrays")
81+
82+
# Indices of triangles containing x, y points, or -1 for no triangles.
83+
tris = self._trifinder(x, y)
84+
85+
# Perform interpolation.
86+
z = self._multi_interp(x, y, tris)
87+
88+
# Return masked array.
89+
return np.ma.masked_invalid(z, copy=False)
90+
91+
def _single_interp(self, x, y, tri):
92+
"""
93+
Return single interpolated value at specified (*x*, *y*) coordinates
94+
within triangle with index *tri*. Returns np.nan if tri == -1.
95+
"""
96+
if tri == -1:
97+
return np.nan
98+
else:
99+
return (self._plane_coefficients[tri,0] * x +
100+
self._plane_coefficients[tri,1] * y +
101+
self._plane_coefficients[tri,2])

0 commit comments

Comments
 (0)