Source code for torx.geometry.polygon_2d_m
"""Implementation of a 2D closed polygon class."""
import numpy as np
from skimage.measure import points_in_poly
from torx.autodoc_decorators_m import autodoc_class
[docs]
@autodoc_class
class Polygon2D:
"""2D polygon class on a poloidal plane."""
[docs]
def __init__(self, x_points: np.ndarray, y_points: np.ndarray,
invert_polygon: bool=False):
"""
Initialize the polygon with the vertices.
The polygon is treated as closed, though the first and last points do
not have to be the same.
If invert_polygon is true, points outside the polygon are considered
"inside," i.e. will return True from the routine points_inside.
"""
self.x_points = np.array(x_points).flatten()
self.y_points = np.array(y_points).flatten()
self.invert_polygon = invert_polygon
assert self.x_points.shape == self.y_points.shape
assert len(self.x_points) == self.y_points.size
[docs]
def __getitem__(self, index):
"""
Allow array indexing.
Like:
polygon[i] → (x, y) at index i
polygon[i, 0] → x at index i
polygon[i, 1] → y at index i
"""
if isinstance(index, tuple):
i, j = index
if j == 0:
return self.x_points[i]
elif j == 1:
return self.y_points[i]
else:
raise IndexError("Index must be 0 or 1 for second dimension")
else:
# Handle single index
x = self.x_points[index]
y = self.y_points[index]
return np.column_stack([x, y])
[docs]
def flip_Z(self):
"""
Flips the vertical direction.
To interface with NUMERICAL equilibria which use this for field
reversal. Also reverses the vertex arrays to
ensure that the points remain defined in the same counter-/clockwise
orientation as before
"""
self.x_points = self.x_points[::-1]
self.y_points = -self.y_points[::-1]
@property
def verts(self):
"""
Return the point vertices in a (M, 2) array.
Compatible with skimage.measure.points_in_poly
"""
return np.column_stack((self.x_points, self.y_points))
[docs]
def points_inside(self, x_tests, y_tests):
"""Check whether points are inside the polygon."""
assert x_tests.shape == y_tests.shape
original_shape = x_tests.shape
points = np.column_stack((x_tests.flatten(), y_tests.flatten()))
mask = points_in_poly(points=points,
verts=self.verts).reshape(original_shape)
if not (self.invert_polygon):
return mask
else:
return np.logical_not(mask)
[docs]
def point_inside(self, x_test, y_test):
"""Return whether a test point is within the polygon."""
return self.points_inside(np.atleast_1d(x_test),
np.atleast_1d(y_test))[0]
[docs]
def signed_area(self):
"""
Return the signed area of a non-intersecting polygon.
If the sign is positive, points are counterclockwise. Otherwise, points
are clockwise.
See https://mathworld.wolfram.com/PolygonArea.html
"""
area = np.sum( self.x_points * np.roll(self.y_points, -1) \
- self.y_points * np.roll(self.x_points, -1) \
) / 2.0
return area
[docs]
def repeat_first_point(self):
"""
Conditionally repeat the first point at the end.
If the polygon is not already closed, adds the final edge to link the
first and last points. Useful for plotting.
"""
if not np.isclose(self.x_points[0], self.x_points[-1]) or \
not np.isclose(self.y_points[0], self.y_points[-1]):
self.x_points = np.append(self.x_points, self.x_points[0])
self.y_points = np.append(self.y_points, self.y_points[0])
[docs]
def to_ordered(self, start_idx: int=0, k: int=20,
n_interp: int=0, smooth: float=None):
"""
Return an OrderedPolygon2D version of this polygon.
Orders the points counterclockwise starting from the point with
index start_idx using a nearest-neighbor algorithm with up to k
nearest neighbors.
"""
from torx.geometry.ordered_polygon_2d_m import OrderedPolygon2D
return OrderedPolygon2D(self.x_points, self.y_points,
start_idx=start_idx, k=k,
n_interp=n_interp, smooth=smooth)
[docs]
@classmethod
def from_2d_array(cls, data_slice: np.ndarray):
"""Create an instance from a single 2D array slice (npoints, xy)."""
if data_slice.shape[-1] != 2:
raise ValueError("The last dimension (dim_xy) must have length 2.")
x_points = data_slice[:, 0]
y_points = data_slice[:, 1]
return cls(x_points, y_points)