Completed
Pull Request — master (#283)
by Ryan
01:32
created

draw_polygon_with_info()   A

Complexity

Conditions 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 0
loc 10
rs 9.4285
1
# Copyright (c) 2008-2016 MetPy Developers.
2
# Distributed under the terms of the BSD 3-Clause License.
3
# SPDX-License-Identifier: BSD-3-Clause
4
"""
5
Natural Neighbor Verification
6
=============================
7
8
Walks through the steps of Natural Neighbor interpolation to validate that the algorithmic
9
approach taken in MetPy is correct.
10
"""
11
###########################################
12
# Find natural neighbors visual test
13
#
14
# A triangle is a natural neighbor for a point if the
15
# `circumscribed circle <https://en.wikipedia.org/wiki/Circumscribed_circle>`_ of the
16
# triangle contains that point. It is important that we correctly grab the correct triangles
17
# for each point before proceeding with the interpolation.
18
#
19
# Algorithmically:
20
#
21
# 1. We place all of the grid points in a KDTree. These provide worst-case O(n) time
22
#    complexity for spatial searches.
23
#
24
# 2. We generate a `Delaunay Triangulation <https://docs.scipy.org/doc/scipy/
25
#    reference/tutorial/spatial.html#delaunay-triangulations>`_
26
#    using the locations of the provided observations.
27
#
28
# 3. For each triangle, we calculate its circumcenter and circumradius. Using
29
#    KDTree, we then assign each grid a triangle that has a circumcenter within a
30
#    circumradius of the grid's location.
31
#
32
# 4. The resulting dictionary uses the grid index as a key and a set of natural
33
#    neighbor triangles in the form of triangle codes from the Delaunay triangulation.
34
#    This dictionary is then iterated through to calculate interpolation values.
35
#
36
# 5. We then traverse the ordered natural neighbor edge vertices for a particular
37
#    grid cell in groups of 3 (n - 1, n, n + 1), and perform calculations to generate
38
#    proportional polygon areas.
39
#
40
#    Circumcenter of (n - 1), n, grid_location
41
#    Circumcenter of (n + 1), n, grid_location
42
#
43
#    Determine what existing circumcenters (ie, Delaunay circumcenters) are associated
44
#    with vertex n, and add those as polygon vertices. Calculate the area of this polygon.
45
#
46
# 6. Increment the current edges to be checked, i.e.:
47
#    n - 1 = n, n = n + 1, n + 1 = n + 2
48
#
49
# 7. Repeat steps 5 & 6 until all of the edge combinations of 3 have been visited.
50
#
51
# 8. Repeat steps 4 through 7 for each grid cell.
52
import matplotlib.pyplot as plt
53
import numpy as np
54
from scipy.spatial import ConvexHull, Delaunay, delaunay_plot_2d, Voronoi, voronoi_plot_2d
55
from scipy.spatial.distance import euclidean
56
57
from metpy.gridding import polygons, triangles
58
from metpy.gridding.interpolation import nn_point
59
60
plt.rcParams['figure.figsize'] = (15, 10)
61
62
###########################################
63
# For a test case, we generate 10 random points and observations, where the
64
# observation values are just the x coordinate value times the y coordinate
65
# value divided by 1000.
66
#
67
# We then create two test points (grid 0 & grid 1) at which we want to
68
# estimate a value using natural neighbor interpolation.
69
#
70
# The locations of these observations are then used to generate a Delaunay triangulation.
71
np.random.seed(100)
72
73
pts = np.random.randint(0, 100, (10, 2))
74
xp = pts[:, 0]
75
yp = pts[:, 1]
76
zp = (pts[:, 0] * pts[:, 0]) / 1000
77
78
tri = Delaunay(pts)
79
delaunay_plot_2d(tri)
80
81
for i, zval in enumerate(zp):
82
    plt.annotate('{} F'.format(zval), xy=(pts[i, 0] + 2, pts[i, 1]))
83
84
sim_gridx = [30., 60.]
85
sim_gridy = [30., 60.]
86
87
plt.plot(sim_gridx, sim_gridy, '+', markersize=10)
88
plt.axes().set_aspect('equal', 'datalim')
89
plt.title('Triangulation of observations and test grid cell '
90
          'natural neighbor interpolation values')
91
92
members, tri_info = triangles.find_natural_neighbors(tri, list(zip(sim_gridx, sim_gridy)))
93
94
val = nn_point(xp, yp, zp, (sim_gridx[0], sim_gridy[0]), tri, members[0], tri_info)
95
plt.annotate('grid 0: {:.3f}'.format(val), xy=(sim_gridx[0] + 2, sim_gridy[0]))
96
97
val = nn_point(xp, yp, zp, (sim_gridx[1], sim_gridy[1]), tri, members[1], tri_info)
98
plt.annotate('grid 1: {:.3f}'.format(val), xy=(sim_gridx[1] + 2, sim_gridy[1]))
99
100
101
###########################################
102
# Using the circumcenter and circumcircle radius information from
103
# :func:`metpy.gridding.triangles.find_natural_neighbors`, we can visually
104
# examine the results to see if they are correct.
105
def draw_circle(x, y, r, m, label):
106
    nx = x + r * np.cos(np.deg2rad(list(range(360))))
107
    ny = y + r * np.sin(np.deg2rad(list(range(360))))
108
    plt.plot(nx, ny, m, label=label)
109
110
111
members, tri_info = triangles.find_natural_neighbors(tri, list(zip(sim_gridx, sim_gridy)))
112
delaunay_plot_2d(tri)
113
plt.plot(sim_gridx, sim_gridy, 'ks', markersize=10)
114
115
for i, info in tri_info.items():
116
    x_t = info['cc'][0]
117
    y_t = info['cc'][1]
118
119
    if i in members[1] and i in members[0]:
120
        draw_circle(x_t, y_t, info['r'], 'm-', str(i) + ': grid 1 & 2')
121
        plt.annotate(str(i), xy=(x_t, y_t), fontsize=15)
122
    elif i in members[0]:
123
        draw_circle(x_t, y_t, info['r'], 'r-', str(i) + ': grid 0')
124
        plt.annotate(str(i), xy=(x_t, y_t), fontsize=15)
125
    elif i in members[1]:
126
        draw_circle(x_t, y_t, info['r'], 'b-', str(i) + ': grid 1')
127
        plt.annotate(str(i), xy=(x_t, y_t), fontsize=15)
128
    else:
129
        draw_circle(x_t, y_t, info['r'], 'k:', str(i) + ': no match')
130
        plt.annotate(str(i), xy=(x_t, y_t), fontsize=9)
131
132
plt.axes().set_aspect('equal', 'datalim')
133
plt.legend()
134
135
###########################################
136
# What?....the circle from triangle 8 looks pretty darn close. Why isn't
137
# grid 0 included in that circle?
138
x_t, y_t = tri_info[8]['cc']
139
r = tri_info[8]['r']
140
141
print('Distance between grid0 and Triangle 8 circumcenter:',
142
      euclidean([x_t, y_t], [sim_gridx[0], sim_gridy[0]]))
143
print('Triangle 8 circumradius:', r)
144
145
###########################################
146
# Lets do a manual check of the above interpolation value for grid 0 (southernmost grid)
147
# Grab the circumcenters and radii for natural neighbors
148
cc = np.array([tri_info[m]['cc'] for m in members[0]])
149
r = np.array([tri_info[m]['r'] for m in members[0]])
150
151
print('circumcenters:\n', cc)
152
print('radii\n', r)
153
154
###########################################
155
# Draw the natural neighbor triangles and their circumcenters. Also plot a `Voronoi diagram
156
# <https://docs.scipy.org/doc/scipy/reference/tutorial/spatial.html#voronoi-diagrams>`_
157
# which serves as a complementary (but not necessary)
158
# spatial data structure that we use here simply to show areal ratios.
159
# Notice that the two natural neighbor triangle circumcenters are also vertices
160
# in the Voronoi plot (green dots), and the observations are in the the polygons (blue dots).
161
vor = Voronoi(list(zip(xp, yp)))
162
voronoi_plot_2d(vor)
163
164
nn_ind = np.array([0, 5, 7, 8])
165
z_0 = zp[nn_ind]
166
x_0 = xp[nn_ind]
167
y_0 = yp[nn_ind]
168
169
for x, y, z in zip(x_0, y_0, z_0):
170
    plt.annotate('{}, {}: {:.3f} F'.format(x, y, z), xy=(x, y))
171
172
plt.plot(sim_gridx[0], sim_gridy[0], 'k+', markersize=10)
173
plt.annotate('{}, {}'.format(sim_gridx[0], sim_gridy[0]), xy=(sim_gridx[0] + 2, sim_gridy[0]))
174
plt.plot(cc[:, 0], cc[:, 1], 'ks', markersize=15, fillstyle='none',
175
         label='natural neighbor\ncircumcenters')
176
177
for center in cc:
178
    plt.annotate('{:.3f}, {:.3f}'.format(center[0], center[1]),
179
                 xy=(center[0] + 1, center[1] + 1))
180
181
tris = tri.points[tri.simplices[members[0]]]
182
for triangle in tris:
183
    x = [triangle[0, 0], triangle[1, 0], triangle[2, 0], triangle[0, 0]]
184
    y = [triangle[0, 1], triangle[1, 1], triangle[2, 1], triangle[0, 1]]
185
    plt.plot(x, y, ':', linewidth=2)
186
187
plt.legend()
188
plt.axes().set_aspect('equal', 'datalim')
189
190
191
def draw_polygon_with_info(polygon, off_x=0, off_y=0):
192
    """Draw one of the natural neighbor polygons with some information."""
193
    pts = np.array(polygon)[ConvexHull(polygon).vertices]
194
    for i, pt in enumerate(pts):
195
        plt.plot([pt[0], pts[(i + 1) % len(pts)][0]],
196
                 [pt[1], pts[(i + 1) % len(pts)][1]], 'k-')
197
198
    avex, avey = np.mean(pts, axis=0)
199
    plt.annotate('area: {:.3f}'.format(polygons.area(pts)), xy=(avex + off_x, avey + off_y),
200
                 fontsize=12)
201
202
203
cc1 = triangles.circumcenter((53, 66), (15, 60), (30, 30))
204
cc2 = triangles.circumcenter((34, 24), (53, 66), (30, 30))
205
draw_polygon_with_info([cc[0], cc1, cc2])
206
207
cc1 = triangles.circumcenter((53, 66), (15, 60), (30, 30))
208
cc2 = triangles.circumcenter((15, 60), (8, 24), (30, 30))
209
draw_polygon_with_info([cc[0], cc[1], cc1, cc2], off_x=-9, off_y=3)
210
211
cc1 = triangles.circumcenter((8, 24), (34, 24), (30, 30))
212
cc2 = triangles.circumcenter((15, 60), (8, 24), (30, 30))
213
draw_polygon_with_info([cc[1], cc1, cc2], off_x=-15)
214
215
cc1 = triangles.circumcenter((8, 24), (34, 24), (30, 30))
216
cc2 = triangles.circumcenter((34, 24), (53, 66), (30, 30))
217
draw_polygon_with_info([cc[0], cc[1], cc1, cc2])
218
219
###########################################
220
# Put all of the generated polygon areas and their affiliated values in arrays.
221
# Calculate the total area of all of the generated polygons.
222
areas = np.array([60.434, 448.296, 25.916, 70.647])
223
values = np.array([0.064, 1.156, 2.809, 0.225])
224
total_area = np.sum(areas)
225
print(total_area)
226
227
###########################################
228
# For each polygon area, calculate its percent of total area.
229
proportions = areas / total_area
230
print(proportions)
231
232
###########################################
233
# Multiply the percent of total area by the respective values.
234
contributions = proportions * values
235
print(contributions)
236
237
###########################################
238
# The sum of this array is the interpolation value!
239
interpolation_value = np.sum(contributions)
240
function_output = nn_point(xp, yp, zp, (sim_gridx[0], sim_gridy[0]), tri, members[0], tri_info)
241
242
print(interpolation_value, function_output)
243
244
###########################################
245
# The values are slightly different due to truncating the area values in
246
# the above visual example to the 3rd decimal place.
247