290 lines
8.9 KiB
Python
290 lines
8.9 KiB
Python
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
from scipy.spatial import Voronoi
|
|
from .cvfdutil import get_disv_gridprops
|
|
|
|
|
|
def get_sorted_vertices(icell_vertices, vertices):
|
|
centroid = vertices[icell_vertices].mean(axis=0)
|
|
tlist = []
|
|
for i, iv in enumerate(icell_vertices):
|
|
x, y = vertices[iv]
|
|
dx = x - centroid[0]
|
|
dy = y - centroid[1]
|
|
tlist.append((np.arctan2(-dy, dx), iv))
|
|
tlist.sort()
|
|
return [iv for angl, iv in tlist]
|
|
|
|
|
|
def get_valid_faces(vor):
|
|
nvalid_faces = np.zeros(vor.npoints, dtype=int)
|
|
for pointidx, simplex in zip(vor.ridge_points, vor.ridge_vertices):
|
|
simplex = np.asarray(simplex)
|
|
if np.all(simplex >= 0):
|
|
ip1, ip2 = pointidx
|
|
nvalid_faces[ip1] += 1
|
|
nvalid_faces[ip2] += 1
|
|
return nvalid_faces
|
|
|
|
|
|
# todo: send this to point in polygon method defined in Rasters
|
|
def point_in_cell(point, vertices):
|
|
from shapely.geometry import Point, Polygon
|
|
|
|
p = Point(point)
|
|
poly = Polygon(vertices)
|
|
if p.intersects(poly):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
# todo: find out how this is different from get_sorted_vertices()
|
|
def sort_vertices(vlist):
|
|
x, y = zip(*vlist)
|
|
x = np.array(x)
|
|
y = np.array(y)
|
|
xc = x.mean()
|
|
yc = y.mean()
|
|
tlist = []
|
|
for i, (x, y) in enumerate(vlist):
|
|
dx = x - xc
|
|
dy = y - yc
|
|
tlist.append((np.arctan2(-dy, dx), i))
|
|
tlist.sort()
|
|
return [vlist[i] for angl, i in tlist]
|
|
|
|
|
|
def get_voronoi_grid(points, **kwargs):
|
|
|
|
# Create the voronoi object
|
|
# Note for a circular region, may need to set qhull_options='Qz'
|
|
vor = Voronoi(points, **kwargs)
|
|
|
|
# Go through and replace -1 values in ridge_vertices
|
|
# with a new point on the boundary
|
|
new_ridge_vertices = []
|
|
new_vor_vertices = [(x, y) for x, y in vor.vertices]
|
|
|
|
for pointidx, simplex in zip(vor.ridge_points, vor.ridge_vertices):
|
|
simplex = np.asarray(simplex)
|
|
if np.all(simplex >= 0):
|
|
new_ridge_vertices.append(list(simplex))
|
|
else:
|
|
# i = simplex[simplex >= 0][0] # finite end Voronoi vertex
|
|
i1 = simplex[0]
|
|
i2 = simplex[1]
|
|
|
|
midpoint = vor.points[pointidx].mean(axis=0)
|
|
x, y = midpoint
|
|
new_vor_vertices.append((x, y))
|
|
ipt = len(new_vor_vertices) - 1
|
|
if i1 > 0:
|
|
new_pair = [i1, ipt]
|
|
else:
|
|
new_pair = [ipt, i2]
|
|
new_ridge_vertices.append(new_pair)
|
|
|
|
# create iverts list
|
|
iverts = []
|
|
for n in range(vor.points.shape[0]):
|
|
iverts.append([])
|
|
for pointidx, simplex in zip(vor.ridge_points, new_ridge_vertices):
|
|
for ipt in simplex:
|
|
if ipt not in iverts[pointidx[0]]:
|
|
iverts[pointidx[0]].append(ipt)
|
|
if ipt not in iverts[pointidx[1]]:
|
|
iverts[pointidx[1]].append(ipt)
|
|
|
|
# If a cell doesn't have any valid faces, then it must be a corner,
|
|
# so add the cell point itself as a vertex
|
|
if True:
|
|
nvalid_faces = get_valid_faces(vor)
|
|
for n in np.where(nvalid_faces == 0)[0]:
|
|
x, y = points[n]
|
|
new_vor_vertices.append((x, y))
|
|
iv = len(new_vor_vertices) - 1
|
|
iverts[n].append(iv)
|
|
|
|
# if cell center is not inside polygon, then add cell center as vertex
|
|
if True:
|
|
for n in range(points.shape[0]):
|
|
poly = []
|
|
ivs = iverts[n]
|
|
for iv in ivs:
|
|
x, y = new_vor_vertices[iv]
|
|
poly.append((x, y))
|
|
poly = sort_vertices(poly)
|
|
xc, yc = points[n]
|
|
if not point_in_cell((xc, yc), poly):
|
|
new_vor_vertices.append((xc, yc))
|
|
iv = len(new_vor_vertices) - 1
|
|
iverts[n].append(iv)
|
|
|
|
verts = np.array(new_vor_vertices)
|
|
for icell in range(len(iverts)):
|
|
iverts[icell] = get_sorted_vertices(iverts[icell], verts)
|
|
|
|
return verts, iverts
|
|
|
|
|
|
class VoronoiGrid:
|
|
"""
|
|
FloPy VoronoiGrid helper class for creating a voronoi model grid from
|
|
an array of input points that define cell centers. The class handles
|
|
boundary cells by closing polygons along the edge, something that cannot
|
|
be done directly with the scipy.spatial.Voronoi class.
|
|
|
|
Parameters
|
|
----------
|
|
points : ndarray
|
|
Two dimensional array of points with x in column 0 and y in column 1.
|
|
These points will become cell centers in the voronoi grid.
|
|
kwargs : dict
|
|
List of additional keyword arguments that will be passed through to
|
|
scipy.spatial.Voronoi. For circular shaped model grids, the
|
|
qhull_options='Qz' option has been found to work well.
|
|
|
|
Notes
|
|
-----
|
|
The points passed into this class are marked as the cell center locations.
|
|
Along the edges, these cell centers do not correspond to the centroid
|
|
location of the cell polygon. Instead, the cell centers are along the
|
|
edge.
|
|
|
|
This class does not yet support holes, which are supported by the Triangle
|
|
class. This is a feature that could be added in the future.
|
|
|
|
"""
|
|
|
|
def __init__(self, points, **kwargs):
|
|
verts, iverts = get_voronoi_grid(points, **kwargs)
|
|
self.points = points
|
|
self.verts = verts
|
|
self.iverts = iverts
|
|
self.ncpl = len(iverts)
|
|
self.nverts = verts.shape[0]
|
|
return
|
|
|
|
def get_disv_gridprops(self):
|
|
"""
|
|
Get a dictionary of arguments that can be passed in to the
|
|
flopy.mf6.ModflowGwfdisv class.
|
|
|
|
Returns
|
|
-------
|
|
disv_gridprops : dict
|
|
Dictionary of arguments than can be unpacked into the
|
|
flopy.mf6.ModflowGwfdisv constructor
|
|
|
|
"""
|
|
disv_gridprops = get_disv_gridprops(
|
|
self.verts, self.iverts, xcyc=self.points
|
|
)
|
|
return disv_gridprops
|
|
|
|
def get_disu5_gridprops(self):
|
|
msg = "This method is not implemented yet."
|
|
raise NotImplementedError(msg)
|
|
|
|
def get_disu6_gridprops(self):
|
|
msg = "This method is not implemented yet."
|
|
raise NotImplementedError(msg)
|
|
|
|
def get_gridprops_vertexgrid(self):
|
|
"""
|
|
Get a dictionary of information needed to create a flopy VertexGrid.
|
|
The returned dictionary can be unpacked directly into the
|
|
flopy.discretization.VertexGrid() constructor.
|
|
|
|
Returns
|
|
-------
|
|
gridprops : dict
|
|
|
|
"""
|
|
gridprops = self.get_disv_gridprops()
|
|
del gridprops["nvert"]
|
|
return gridprops
|
|
|
|
def get_gridprops_unstructuredgrid(self):
|
|
"""
|
|
Get a dictionary of information needed to create a flopy
|
|
UnstructuredGrid. The returned dictionary can be unpacked directly
|
|
into the flopy.discretization.UnstructuredGrid() constructor.
|
|
|
|
Returns
|
|
-------
|
|
gridprops : dict
|
|
|
|
"""
|
|
gridprops = {}
|
|
|
|
disv_gridprops = self.get_disv_gridprops()
|
|
vertices = disv_gridprops["vertices"]
|
|
iverts = self.iverts
|
|
ncpl = self.ncpl
|
|
xcenters = self.points[:, 0]
|
|
ycenters = self.points[:, 1]
|
|
|
|
gridprops["vertices"] = vertices
|
|
gridprops["iverts"] = iverts
|
|
gridprops["ncpl"] = ncpl
|
|
gridprops["xcenters"] = xcenters
|
|
gridprops["ycenters"] = ycenters
|
|
|
|
return gridprops
|
|
|
|
def get_patch_collection(self, ax=None, **kwargs):
|
|
"""
|
|
Get a matplotlib patch collection representation of the voronoi grid
|
|
|
|
Parameters
|
|
----------
|
|
ax : matplotlib.pyplot.Axes
|
|
axes to plot the patch collection
|
|
kwargs : dict
|
|
Additional keyward arguments to pass to the flopy.plot.plot_cvfd
|
|
function that returns a patch collection from verts and iverts
|
|
|
|
Returns
|
|
-------
|
|
pc : matplotlib.collections.PatchCollection
|
|
patch collection of model
|
|
|
|
"""
|
|
from ..discretization import VertexGrid
|
|
from ..plot import PlotMapView
|
|
|
|
modelgrid = VertexGrid(**self.get_gridprops_vertexgrid())
|
|
pmv = PlotMapView(modelgrid=modelgrid, ax=ax)
|
|
pc = pmv.plot_grid(**kwargs)
|
|
return pc
|
|
|
|
def plot(self, ax=None, plot_title=True, **kwargs):
|
|
"""
|
|
Plot the voronoi model grid
|
|
|
|
Parameters
|
|
----------
|
|
ax : matplotlib.pyplot.Axes
|
|
axes to plot the voronoi grid
|
|
plot_title : bool
|
|
Add the number of cells and number of vertices as a plot title
|
|
kwargs : dict
|
|
Additional keyword arguments to pass to self.get_patch_collection
|
|
|
|
Returns
|
|
-------
|
|
ax : matplotlib.pyplot.Axes
|
|
axes that contains the voronoi model grid
|
|
|
|
"""
|
|
if ax is None:
|
|
ax = plt.subplot(1, 1, 1, aspect="equal")
|
|
pc = self.get_patch_collection(ax, **kwargs)
|
|
if plot_title:
|
|
ax.set_title(
|
|
"ncells: {}; nverts: {}".format(self.ncpl, self.nverts)
|
|
)
|
|
return ax
|