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
|
|
|
|