Source code for xdesign.geometry.area

"""Define two dimensional geometric entities."""

__author__ = "Daniel Ching, Doga Gursoy"
__copyright__ = "Copyright (c) 2016, UChicago Argonne, LLC."
__docformat__ = 'restructuredtext en'
__all__ = [
    'Curve',
    'Circle',
    'Polygon',
    'RegularPolygon',
    'Triangle',
    'Rectangle',
    'Square',
    'Mesh',
]

from copy import deepcopy
import logging
import warnings

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.path import Path
from cached_property import cached_property

from xdesign.geometry.entity import *
from xdesign.geometry.line import *
from xdesign.geometry.point import *

logger = logging.getLogger(__name__)


[docs]class Curve(Entity): """The base class for closed manifolds defined by a single equation. e.g. :class:`.Circle`, :class:`.Sphere`, or :class:`.Torus`. Attributes ---------- center : Point """ def __init__(self, center): if not isinstance(center, Point): raise TypeError("center must be a Point.") super(Curve, self).__init__() self.center = center def __repr__(self): return "{}(center={})".format(type(self).__name__, repr(self.center))
[docs] def translate(self, vector): """Translates the Curve along a vector.""" if not isinstance(vector, (Point, list, np.array)): raise TypeError("vector must be point, list, or array.") self.center.translate(vector)
[docs] def rotate(self, theta, point=None, axis=None): """Rotates the Curve by theta radians around an axis which passes through a point radians.""" self.center.rotate(theta, point, axis)
class Superellipse(Curve): """A Superellipse in 2D cartesian space. Attributes ---------- center : Point a : scalar b : scalar n : scalar """ def __init__(self, center, a, b, n): super(Superellipse, self).__init__(center) self.a = float(a) self.b = float(b) self.n = float(n) def __repr__(self): return "Superellipse(center={}, a={}, b={}, n={})".format( repr(self.center), repr(self.a), repr(self.b), repr(self.n) ) @property def list(self): """Return list representation.""" return [self.center.x, self.center.y, self.a, self.b, self.n] def scale(self, val): """Scale.""" self.a *= val self.b *= val class Ellipse(Superellipse): """Ellipse in 2-D cartesian space. Attributes ---------- center : Point a : scalar b : scalar """ def __init__(self, center, a, b): super(Ellipse, self).__init__(center, a, b, 2) def __repr__(self): return "Ellipse(center={}, a={}, b={})".format( repr(self.center), repr(self.a), repr(self.b) ) @property def list(self): """Return list representation.""" return [self.center.x, self.center.y, self.a, self.b] @property def area(self): """Return area.""" return np.pi * self.a * self.b def scale(self, val): """Scale.""" self.a *= val self.b *= val
[docs]class Circle(Curve): """Circle in 2D cartesian space. Attributes ---------- center : Point The center point of the circle. radius : scalar The radius of the circle. sign : int (-1 or 1) The sign of the area """ def __init__(self, center, radius, sign=1): super(Circle, self).__init__(center) self.radius = float(radius) self.sign = sign self._dim = 2 def __repr__(self): return "Circle(center={}, radius={}, sign={})".format( repr(self.center), repr(self.radius), repr(self.sign) ) def __str__(self): """Return the analytical equation.""" return "(x-%s)^2 + (y-%s)^2 = %s^2" % ( self.center.x, self.center.y, self.radius ) def __eq__(self, circle): return ((self.x, self.y, self.radius) == (circle.x, circle.y, circle.radius)) def __neg__(self): copE = deepcopy(self) copE.sign = -copE.sign return copE @property def list(self): """Return list representation for saving to files.""" return [self.center.x, self.center.y, self.radius] @property def circumference(self): """Returns the circumference.""" return 2 * np.pi * self.radius @property def diameter(self): """Returns the diameter.""" return 2 * self.radius @property def area(self): """Return the area.""" return self.sign * np.pi * self.radius**2 @property def patch(self): """Returns a matplotlib patch.""" return plt.Circle((self.center.y, self.center.x), self.radius) @property def bounding_box(self): """Return the axis-aligned bounding box as two numpy vectors.""" xmin = np.array(self.center._x - self.radius) xmax = np.array(self.center._x + self.radius) return xmin, xmax # def scale(self, val): # """Scale.""" # raise NotImplementedError # self.center.scale(val) # self.rad *= val
[docs] def contains(self, other): """Return whether `other` is a proper subset. Return one boolean for all geometric entities. Return an array of boolean for array input. """ if isinstance(other, Point): x = other._x elif isinstance(other, np.ndarray): x = other elif isinstance(other, Mesh): for face in other.faces: if not self.contains(face) and face.sign == 1: return False return True else: if self.sign == 1: if other.sign == -1: # Closed shape cannot contain infinite one return False else: assert other.sign == 1 # other is within A if isinstance(other, Circle): return ( other.center.distance(self.center) + other.radius < self.radius ) elif isinstance(other, Polygon): x = _points_to_array(other.vertices) return np.all(self.contains(x)) elif self.sign == -1: if other.sign == 1: # other is outside A and not around if isinstance(other, Circle): return ( other.center.distance(self.center) - other.radius > self.radius ) elif isinstance(other, Polygon): x = _points_to_array(other.vertices) return ( np.all(self.contains(x)) and not other.contains(-self) ) else: assert other.sign is -1 # other is around A if isinstance(other, Circle): return ( other.center.distance(self.center) + self.radius < other.radius ) elif isinstance(other, Polygon): return (-other).contains(-self) x = np.atleast_2d(x) if self.sign == 1: return np.sum((x - self.center._x)**2, axis=1) < self.radius**2 else: return np.sum((x - self.center._x)**2, axis=1) > self.radius**2
def _points_to_array(points): a = np.zeros((len(points), points[0].dim)) for i in range(len(points)): a[i] = points[i]._x return np.atleast_2d(a)
[docs]class Polygon(Entity): """A convex polygon in 2D cartesian space. It is defined by a number of distinct vertices of class :class:`.Point`. Superclasses include :class:`.Square`, :class:`.Triangle`, etc. Attributes ---------- vertices : List of Points sign : int (-1 or 1) The sign of the area Raises ------ ValueError : If the number of vertices is less than three. """ def __init__(self, vertices, sign=1): for v in vertices: if not isinstance(v, Point): raise TypeError("vertices must be of type Point.") if len(vertices) < 3: raise ValueError("A Polygon has at least three vertices.") super(Polygon, self).__init__() self.vertices = vertices self._dim = vertices[0].dim self.sign = sign def __repr__(self): return "Polygon(vertices={}, sign={})".format( repr(self.vertices), repr(self.sign) ) def __str__(self): return "{}({})".format(type(self).__name__, str(self.numpy)) def __neg__(self): copE = deepcopy(self) copE.sign = -copE.sign return copE @property def numverts(self): return len(self.vertices) @property def list(self): """Return list representation.""" lst = [] for m in range(self.numverts): lst.append(self.vertices[m].list) return lst @property def numpy(self): """Return Numpy representation.""" return _points_to_array(self.vertices) @property def patch(self): """Returns a matplotlib patch.""" points = self.vertices a = np.zeros((len(points), points[0].dim)) for i in range(len(points)): a[i] = np.flip(points[i]._x, 0) return plt.Polygon(a) # Cached Properties @property def bounds(self): """Returns a 4-tuple (xmin, ymin, xmax, ymax) representing the bounding rectangle for the Polygon. """ warnings.warn( "Polygon.bounds is deprecated; use Polygon.bounding_box instead.", DeprecationWarning) xs = [p.x for p in self.vertices] ys = [p.y for p in self.vertices] return (min(xs), min(ys), max(xs), max(ys)) @property def bounding_box(self): """Return the axis-aligned bounding box as two numpy vectors.""" xs = [p.x for p in self.vertices] ys = [p.y for p in self.vertices] return np.array([min(xs), min(ys)]), np.array([max(xs), max(ys)]) @property def edges(self): """Return a list of lines connecting the points of the Polygon.""" edges = [] for i in range(self.numverts): edges.append( Segment( self.vertices[i], self.vertices[(i + 1) % self.numverts] ) ) return edges @cached_property def area(self): """Return the area of the Polygon. References ---------- https://en.wikipedia.org/wiki/Shoelace_formula https://stackoverflow.com/a/30408825 """ a = _points_to_array(self.vertices) x = a[:, 0] y = a[:, 1] return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) @cached_property def perimeter(self): """Return the perimeter of the Polygon.""" perimeter = 0 verts = self.vertices points = verts + [verts[0]] for m in range(self.numverts): perimeter += points[m].distance(points[m + 1]) return perimeter @cached_property def center(self): """The center of the bounding circle.""" center = Point(np.zeros(self._dim)) for v in self.vertices: center += v return center / self.numverts @cached_property def radius(self): """The radius of the bounding circle.""" r = 0 c = self.center for m in range(self.numverts): r = max(r, self.vertices[m].distance(c)) return r @cached_property def half_space(self): """Returns the half space polytope respresentation of the polygon.""" assert (self.dim > 0), self.dim A = np.ndarray((self.numverts, self.dim)) B = np.ndarray(self.numverts) for i in range(0, self.numverts): edge = Line( self.vertices[i], self.vertices[(i + 1) % self.numverts] ) A[i, :], B[i] = edge.standard # test for positive or negative side of line if self.center._x.dot(A[i, :]) > B[i]: A[i, :] = -A[i, :] B[i] = -B[i] return A, B # Methods
[docs] def translate(self, vector): """Translates the polygon by a vector.""" for v in self.vertices: v.translate(vector) if 'center' in self.__dict__: self.center.translate(vector) # if 'bounds' in self.__dict__: # self.bounds.translate(vector) if 'half_space' in self.__dict__: self.half_space = self.half_space.translation(vector)
[docs] def rotate(self, theta, point=None, axis=None): """Rotates the Polygon around an axis which passes through a point by theta radians.""" for v in self.vertices: v.rotate(theta, point, axis) if 'center' in self.__dict__: self.center.rotate(theta, point, axis) # if 'bounds' in self.__dict__: # self.bounds.rotate(theta, point, axis) if 'half_space' in self.__dict__: if point is None: d = 0 else: d = point._x self.half_space = self.half_space.translation(-d) self.half_space = self.half_space.rotation(0, 1, theta) self.half_space = self.half_space.translation(d)
[docs] def contains(self, other): """Return whether this Polygon contains the other.""" if isinstance(other, Point): x = other._x elif isinstance(other, np.ndarray): x = other elif isinstance(other, Mesh): for face in other.faces: if not self.contains(face) and face.sign == 1: return False return True else: if self.sign == 1: if other.sign == -1: # Closed shape cannot contain infinite one return False else: assert other.sign == 1 # other is within A if isinstance(other, Circle): if self.contains(other.center): for edge in self.edges: if other.center.distance(edge) < other.radius: return False return True return False elif isinstance(other, Polygon): x = _points_to_array(other.vertices) return np.all(self.contains(x)) elif self.sign == -1: if other.sign == 1: # other is outside A and not around if isinstance(other, Circle): if self.contains(other.center): for edge in self.edges: if other.center.distance(edge) < other.radius: return False return True and not other.contains(-self) return False elif isinstance(other, Polygon): x = _points_to_array(other.vertices) return ( np.all(self.contains(x)) and not other.contains(-self) ) else: assert other.sign is -1 # other is around A if isinstance(other, Circle) or isinstance(other, Polygon): return (-other).contains(-self) border = Path(self.numpy) if self.sign == 1: return border.contains_points(np.atleast_2d(x)) else: return np.logical_not(border.contains_points(np.atleast_2d(x)))
[docs]class RegularPolygon(Polygon): """A regular polygon in 2D cartesian space. It is defined by the polynomial center, order, and radius. By default (i.e. when the ``angle`` parameter is zero), the regular polygon is oriented so that one of the vertices is at coordinates :math:`(x + r, x)` where :math:`x` is the x-coordinate of ``center`` and :math:`r` = ``radius``. The ``angle`` parameter is only meaningful modulo :math:`2\pi /` ``order`` since rotation by :math:`2\pi /` ``order`` gives a result equivalent to no rotation. Parameters ---------- center : :class:`Point` The center of the polygon radius : float Distance from polygon center to vertices order : int Order of the polygon (e.g. order 6 is a hexagon). angle : float Optional rotation angle in radians. sign : int (-1 or 1) Optional sign of the area (see :class:`Polygon`) """ def __init__(self, center, radius, order, angle=0, sign=1): vertex_angles = (np.linspace(0, 2 * np.pi, order, endpoint=False) + angle) vertices = [ Point([radius * np.cos(theta), radius * np.sin(theta)]) + center for theta in vertex_angles ] super(RegularPolygon, self).__init__(vertices, sign=sign)
[docs]class Triangle(Polygon): """Triangle in 2D cartesian space. It is defined by three distinct points. """ def __init__(self, p1, p2, p3): super(Triangle, self).__init__([p1, p2, p3]) def __repr__(self): return "Triangle({}, {}, {})".format( self.vertices[0], self.vertices[1], self.vertices[2] ) @cached_property def center(self): center = Point([0, 0]) for v in self.vertices: center += v return center / 3 @cached_property def area(self): A = self.vertices[0] - self.vertices[1] B = self.vertices[0] - self.vertices[2] return self.sign * 1 / 2 * np.abs(np.cross([A.x, A.y], [B.x, B.y]))
[docs]class Rectangle(Polygon): """Rectangle in 2D cartesian space. Defined by a point and a vector to enforce perpendicular sides. Parameters ---------- side_lengths : array The lengths of the sides """ def __init__(self, center, side_lengths): s = np.array(side_lengths) / 2 self.side_lengths = np.array(side_lengths) p1 = Point([center.x + s[0], center.y + s[1]]) p2 = Point([center.x - s[0], center.y + s[1]]) p3 = Point([center.x - s[0], center.y - s[1]]) p4 = Point([center.x + s[0], center.y - s[1]]) super(Rectangle, self).__init__([p1, p2, p3, p4]) def __repr__(self): return "Rectangle({}, {})".format( repr(self.center), repr(self.side_lengths.tolist()) ) @cached_property def area(self): return self.sign * ( self.vertices[0].distance(self.vertices[1]) * self.vertices[1].distance(self.vertices[2]) )
[docs]class Square(Rectangle): """Square in 2D cartesian space. Defined by a point and a length to enforce perpendicular sides. """ def __init__(self, center, side_length=None, radius=None): if radius is not None: # side_length = np.sqrt(2) * radius side_length = 2 * radius side_lengths = [side_length] * 2 super(Square, self).__init__(center, side_lengths)
[docs]class Mesh(Entity): """A collection of Entities Attributes ---------- faces : :py:obj:`list` A list of the Entities area : float The total area of the Entities population : int The number entities in the Mesh radius : float The radius of a bounding circle """ def __init__(self, obj=None, faces=[]): self.faces = [] self.area = 0 self.population = 0 self.radius = 0 self._dim = 2 if obj is not None: assert not faces self.import_triangle(obj) else: assert obj is None for face in faces: self.append(face) def __str__(self): return "Mesh(" + str(self.center) + ")" def __repr__(self): return "Mesh(faces={})".format(repr(self.faces))
[docs] def import_triangle(self, obj): """Loads mesh data from a Python Triangle dict. """ for face in obj['triangles']: p0 = Point(obj['vertices'][face[0], 0], obj['vertices'][face[0], 1]) p1 = Point(obj['vertices'][face[1], 0], obj['vertices'][face[1], 1]) p2 = Point(obj['vertices'][face[2], 0], obj['vertices'][face[2], 1]) t = Triangle(p0, p1, p2) self.append(t)
@property def center(self): center = Point([0, 0]) if self.area > 0: for f in self.faces: center += f.center * f.area center /= self.area return center @property def bounding_box(self): """Return the axis-aligned bounding box as two numpy vectors.""" xmin = np.full(self.dim, np.nan) xmax = np.full(self.dim, np.nan) for f in self.faces: fmin, fmax = f.bounding_box with np.errstate(invalid='ignore'): xmin = np.fmin(xmin, fmin) xmax = np.fmax(xmax, fmax) return xmin, xmax
[docs] def append(self, t): """Add a triangle to the mesh.""" self.population += 1 # self.center = ((self.center * self.area + t.center * t.area) / # (self.area + t.area)) self.area += t.area if isinstance(t, Polygon): for v in t.vertices: self.radius = max(self.radius, self.center.distance(v)) else: self.radius = max( self.radius, self.center.distance(t.center) + t.radius ) self.faces.append(t)
[docs] def pop(self, i=-1): """Pop i-th triangle from the mesh.""" self.population -= 1 self.area -= self.faces[i].area try: del self.__dict__['center'] except KeyError: pass return self.faces.pop(i)
[docs] def translate(self, vector): """Translate entity.""" for t in self.faces: t.translate(vector)
[docs] def rotate(self, theta, point=None, axis=None): """Rotate entity around an axis which passes through a point by theta radians.""" for t in self.faces: t.rotate(theta, point, axis)
[docs] def scale(self, vector): """Scale entity.""" for t in self.faces: t.scale(vector)
[docs] def contains(self, other): """Return whether this Mesh contains other. FOR ALL `x`, THERE EXISTS a face of the Mesh that contains `x` AND (ALL cut outs that contain `x` or THERE DOES NOT EXIST a cut out). """ if isinstance(other, Point): x = other._x elif isinstance(other, np.ndarray): x = other elif isinstance(other, Polygon): x = _points_to_array(other.vertices) return np.all(self.contains(x)) elif isinstance(other, Circle): warnings.warn("Didn't check that Mesh contains Circle.") return True else: raise NotImplementedError("Mesh.contains({})".format(type(other))) x = np.atleast_2d(x) # keep track of whether each point is contained in a face x_in_face = np.zeros(x.shape[0], dtype=bool) x_in_cut = np.zeros(x.shape[0], dtype=bool) has_cuts = False for f in self.faces: if f.sign < 0: has_cuts = True x_in_cut = np.logical_or(x_in_cut, f.contains(x)) else: x_in_face = np.logical_or(x_in_face, f.contains(x)) if has_cuts: return np.logical_and(x_in_face, x_in_cut) else: return x_in_face
@property def patch(self): patches = [] for f in self.faces: patches.append(f.patch) return patches