Source code for sknano.core.geometric_regions._2D_regions

# -*- coding: utf-8 -*-
"""
=======================================================================
2D geometric regions (:mod:`sknano.core.geometric_regions._2D_regions`)
=======================================================================

.. currentmodule:: sknano.core.geometric_regions._2D_regions

"""
from __future__ import absolute_import, division, print_function, \
    unicode_literals
__docformat__ = 'restructuredtext en'

from abc import ABCMeta, abstractmethod

import numpy as np

from sknano.core.math import Point, Vector
from ._base import GeometricRegion, GeometricTransformsMixin

__all__ = ['Geometric2DRegion', 'Parallelogram', 'Rectangle', 'Square',
           'Ellipse', 'Circle', 'Triangle']


[docs]class Geometric2DRegion(GeometricRegion, GeometricTransformsMixin, metaclass=ABCMeta): """Abstract base class for representing 2D geometric regions.""" @property @abstractmethod def area(self): """Area of 2D geometric region.""" raise NotImplementedError @property def measure(self): """Alias for :attr:`~Geometric2DRegion.area`, which is the measure \ of a 2D geometric region.""" return self.area
[docs]class Parallelogram(Geometric2DRegion): """`Geometric2DRegion` for a parallelogram. .. versionadded:: 0.3.0 Represents a parallelogram with origin :math:`o=(o_x, o_y)` and direction vectors :math:`\\mathbf{u}=(u_x, u_y)` and :math:`\\mathbf{v}=(v_x, v_y)`. Parameters ---------- o : array_like, optional Parallelogram origin. If `None`, it defaults to `o=[0, 0]`. u, v : array_like, optional Parallelogram direction vectors stemming from origin `o`. If `None`, then the default values are `u=[1, 0]` and `v=[1, 1]`. Notes ----- :class:`Parallelogram` represents the bounded region :math:`\\left \\{o+\\lambda_1\\mathbf{u}+\\lambda_2\\mathbf{v}\\in R^2 |0\\le\\lambda_i\\le 1\\right\\}`, where :math:`\\mathbf{u}` and :math:`\\mathbf{v}` have to be linearly independent. Calling :class:`Paralleogram` with no parameters is equivalent to :class:`Parallelogram`\ `(o=[0, 0], u=[1, 0], v=[1, 1])` """ def __init__(self, o=None, u=None, v=None): super().__init__() self._o = Point(nd=2) self._u = Vector(nd=2) self._v = Vector(nd=2) if o is None: o = [0, 0] self.o = o if u is None: u = [1, 0] self.u = u if v is None: v = [1, 1] self.v = v self.points.append(self.o) self.vectors.extend([self.u, self.v]) self.fmtstr = "o={o!r}, u={u!r}, v={v!r}" @property def o(self): """2D point coordinates :math:`(o_x, o_y)` of origin. Returns ------- :class:`Point` 2D :class:`Point` coordinates :math:`(o_x, o_y)` of origin. """ return self._o @o.setter def o(self, value): if not isinstance(value, (tuple, list, np.ndarray)) or len(value) != 2: raise TypeError('Expected a 2-element array_like object') self.vectors.translate(Vector(p0=self.o, p=Point(value))) self._o[:] = Point(value) @property def u(self): """2D direction vector :math:`\\mathbf{u}=(u_x, u_y)`, with origin \ :attr:`~Paralleogram.o`""" return self._u @u.setter def u(self, value): if not isinstance(value, (tuple, list, np.ndarray)) or len(value) != 2: raise TypeError('Expected a 2-element array_like object') self._u[:] = Vector(value, p0=self.o) @property def v(self): """2D direction vector :math:`\\mathbf{v}=(v_x, v_y)`, with origin \ :attr:`~Paralleogram.o`""" return self._v @v.setter def v(self, value): if not isinstance(value, (tuple, list, np.ndarray)) or len(value) != 2: raise TypeError('Expected a 2-element array_like object') self._v[:] = Vector(value, p0=self.o) @property def area(self): """:class:`Paralleogram` area, \ :math:`A=|\\mathbf{u}\\times\\mathbf{v}|`. Computed as: .. math:: A = |\\mathbf{u}\\times\\mathbf{v}| """ u = self.u v = self.v return np.abs(np.cross(u, v)) @property def centroid(self): """Paralleogram centroid, :math:`(c_x, c_y)`. Computed as the 2D point :math:`(c_x, c_y)` with coordinates: .. math:: c_x = o_x + \\frac{u_x + v_x}{2} c_y = o_y + \\frac{u_y + v_y}{2} where :math:`(o_x, o_y)`, :math:`(u_x, u_y)`, and :math:`(v_x, v_y)` are the :math:`(x, y)` coordinates of the origin :math:`o` and :math:`(x, y)` components of the direction vectors :math:`\\mathbf{u}` and :math:`\\mathbf{v}`, respectively. Returns ------- :class:`Point` 2D :class:`Point` of centroid. """ ox, oy = self.o ux, uy = self.u vx, vy = self.v cx = ox + (ux + vx) / 2 cy = oy + (uy + vy) / 2 return Point([cx, cy])
[docs] def contains(self, point): """Test region membership of `point` in :class:`Parallelogram`. Parameters ---------- point : array_like Returns ------- :class:`~python:bool` `True` if `point` is within :class:`Paralleogram`, `False` otherwise Notes ----- A `point` :math:`(p_x, p_y)` is within the bounded region of a parallelogram with origin :math:`(o_x, o_y)` and direction vectors :math:`\\mathbf{u}=(u_x, u_y)` and :math:`\\mathbf{v}=(v_x, v_y)` if the following is true: .. math:: 0\\le\\frac{(p_y - o_y) v_x + (o_x - p_x) v_y}{u_y v_x - u_x v_y} \\le 1 \\land 0\\le\\frac{(p_y - o_y) u_x + (o_x - p_x) u_y}{u_x v_y - u_y v_x} \\le 1 """ px, py = Point(point) ox, oy = self.o ux, uy = self.u vx, vy = self.v q1 = ((py - oy) * vx + (ox - px) * vy) / (uy * vx - ux * vy) q2 = ((py - oy) * ux + (ox - px) * uy) / (ux * vy - uy * vx) return q1 >= 0 and q1 <= 1 and q2 >= 0 and q2 <= 1
[docs] def todict(self): """Returns a :class:`~python:dict` of the :class:`Paralleogram` \ constructor parameters.""" return dict(o=self.o, u=self.u, v=self.v)
[docs]class Rectangle(Geometric2DRegion): """`Geometric2DRegion` for a rectangle. .. versionadded:: 0.3.0 Represents an axis-aligned bounded region from :math:`p_{\\mathrm{min}}=(x_{\\mathrm{min}},y_{\\mathrm{min}})` to :math:`p_{\\mathrm{max}}=(x_{\\mathrm{max}},y_{\\mathrm{max}})`. Parameters ---------- pmin, pmax : array_like, optional The minimum and maximum 2D point coordinates of the axis-aligned rectangle from `pmin=[xmin, ymin]` to `pmax=[xmax, ymax]`. xmin, ymin : float, optional The minimum :math:`(x, y)` point of the axis-aligned rectangle. xmax, ymax : float, optional The maximum :math:`(x, y)` point of the axis-aligned rectangle. Notes ----- :class:`Rectangle` represents the region :math:`\\left\\{\\{x, y\\}|x_{\\mathrm{min}}\\le x\\le x_{\\mathrm{max}} \\land y_{\\mathrm{min}}\\le y\\le y_{\\mathrm{max}}\\right\\}` Calling :class:`Rectangle` with no parameters is equivalent to :class:`Rectangle`\ `(pmin=[0, 0], pmax=[1, 1])`. """ def __init__(self, pmin=None, pmax=None, xmin=0, ymin=0, xmax=1, ymax=1): super().__init__() self._pmin = Point(nd=2) self._pmax = Point(nd=2) if pmin is None: pmin = [xmin, ymin] self.pmin = pmin if pmax is None: pmax = [xmax, ymax] self.pmax = pmax self.points.append([self.pmin, self.pmax]) self.fmtstr = "pmin={pmin!r}, pmax={pmax!r}" @property def pmin(self): """2D :class:`Point` at \ (:attr:`~Rectangle.xmin`, :attr:`~Rectangle.ymin`).""" return self._pmin @pmin.setter def pmin(self, value): if not isinstance(value, (tuple, list, np.ndarray)) or len(value) != 2: raise TypeError('Expected a 2-element array_like object') self._pmin[:] = Point(value) @property def pmax(self): """2D :class:`Point` at \ (:attr:`~Rectangle.xmax`, :attr:`~Rectangle.ymax`).""" return self._pmax @pmax.setter def pmax(self, value): if not isinstance(value, (tuple, list, np.ndarray)) or len(value) != 2: raise TypeError('Expected a 2-element array_like object') self._pmax[:] = Point(value) @property def xmin(self): """:math:`x_{\\mathrm{min}}` coordinate.""" return self.pmin.x @xmin.setter def xmin(self, value): self.pmin.x = float(value) @property def xmax(self): """:math:`x_{\\mathrm{max}}` coordinate.""" return self.pmax.x @xmax.setter def xmax(self, value): self.pmax.x = float(value) @property def ymin(self): """:math:`y_{\\mathrm{min}}` coordinate.""" return self.pmin.y @ymin.setter def ymin(self, value): self.pmin.y = float(value) @property def ymax(self): """:math:`y_{\\mathrm{max}}` coordinate.""" return self.pmax.y @ymax.setter def ymax(self, value): self.pmax.y = float(value) @property def a(self): """Distance between :math:`x_{\\mathrm{max}}-x_{\\mathrm{min}}`.""" return self.xmax - self.xmin @property def b(self): """Distance between :math:`y_{\\mathrm{max}}-y_{\\mathrm{min}}`.""" return self.ymax - self.ymin @property def area(self): """:class:`Rectangle` area, :math:`A=ab`""" a = self.a b = self.b return a * b @property def centroid(self): """:class:`Rectangle` centroid, :math:`(c_x, c_y)`. Computed as the 2D :class:`Point` :math:`(c_x, c_y)` with coordinates: .. math:: c_x = \\frac{x_{\\mathrm{min}}+x_{\\mathrm{max}}}{2} c_y = \\frac{y_{\\mathrm{min}}+y_{\\mathrm{max}}}{2} Returns ------- :class:`Point` 2D :class:`Point` of centroid. """ cx = (self.xmax + self.xmin) / 2 cy = (self.ymax + self.ymin) / 2 return Point([cx, cy])
[docs] def contains(self, point): """Test region membership of `point` in :class:`Rectangle`. Parameters ---------- point : array_like Returns ------- :class:`~python:bool` `True` if `point` is within :class:`Rectangle`, `False`, otherwise. Notes ----- A point :math:`(p_x, p_y)` is within the bounded region of a rectangle with lower corner at :math:`p_{\\mathrm{min}}= (x_{\\mathrm{min}}, y_{\\mathrm{min}})` and upper corner at :math:`p_{\\mathrm{max}}= (x_{\\mathrm{max}}, y_{\\mathrm{max}})` if the following is true: .. math:: x_{\mathrm{min}}\\le x\\le x_{\\mathrm{max}}\\land y_{\mathrm{min}}\\le y\\le y_{\\mathrm{max}} """ px, py = Point(point) xmin = self.xmin xmax = self.xmax ymin = self.ymin ymax = self.ymax return (px >= xmin) and (px <= xmax) and (py >= ymin) and (py <= ymax)
[docs] def todict(self): """Returns a :class:`~python:dict` of the :class:`Rectangle` \ constructor parameters.""" return dict(pmin=self.pmin, pmax=self.pmax)
[docs]class Square(Geometric2DRegion): """`Geometric2DRegion` for a square. .. versionadded:: 0.3.0 Represents the axis-aligned bounded region with center :math:`(c_x, c_y)` and side length :math:`a`. Parameters ---------- center : array_like, optional The center point coordinate :math:`(c_x, c_y)` of the axis-aligned square. a : float, optional The side length :math:`a` of the axis-aligned square. Notes ----- :class:`Square` represents the region :math:`\\left\\{\\{c_i\\pm\\frac{a}{2}\\}|a>0\\forall i\\in\\{x,y\\}\\right\\}`. Calling :class:`Square` with no parameters is equivalent to :class:`Square`\ `(center=[0, 0], a=1)`. """ def __init__(self, center=None, a=1): super().__init__() self._center = Point(nd=2) if center is None: center = [0, 0] self.center = center self.a = a self.points.append(self.center) self.fmtstr = "center={center!r}, a={a:.2f}" @property def center(self): """Center point :math:`(c_x, c_y)` of axis-aligned square.""" return self._center @center.setter def center(self, value): if not isinstance(value, (tuple, list, np.ndarray)) or len(value) != 2: raise TypeError('Expected a 2-element array_like object') self._center[:] = Point(value) @property def a(self): """Side length :math:`a` of the axis-aligned square.""" return self._a @a.setter def a(self, value): self._a = float(value) @property def area(self): """:class:`Square` area, :math:`A=a^2`.""" return self.a ** 2 @property def centroid(self): """Alias for :attr:`~Square.center`.""" return self.center
[docs] def contains(self, point): """Test region membership of `point` in :class:`Square`. Parameters ---------- point : array_like Returns ------- :class:`~python:bool` `True` if `point` is within :class:`Square`, `False` otherwise. Notes ----- A `point` :math:`(p_x, p_y)` is within the bounded region of a square with center :math:`(c_x, c_y)` and side length :math:`a` if the following is true: .. math:: c_i - \\frac{a}{2}\\le p_i\\le c_i + \\frac{a}{2}\\forall i\\in \\{x, y\\} """ px, py = Point(point) cx, cy = self.center a = self.a xmin = cx - a / 2 xmax = cx + a / 2 ymin = cy - a / 2 ymax = cy + a / 2 return (px >= xmin) and (px <= xmax) and (py >= ymin) and (py <= ymax)
[docs] def todict(self): """Returns a :class:`~python:dict` of the :class:`Square` \ constructor parameters.""" return dict(center=self.center, a=self.a)
[docs]class Triangle(Geometric2DRegion): """`Geometric2DRegion` for a triangle. .. versionadded:: 0.3.10 Represents the bounded region with corner points :math:`p_1=(x_1,y_1)`, :math:`p_2=(x_2,y_2)`, and :math:`p_3=(x_3,y_3)`. Parameters ---------- p1, p2, p3 : array_like, optional 2-tuples or :class:`~sknano.core.Point` class instances specifying the `Triangle` corner points :math:`p_1=(x_1,y_1)`, :math:`p_2=(x_2,y_2)`, and :math:`p_3=(x_3, y_3)`. Notes ----- :class:`Triangle` represents a 2D geometric region consisting of all combinations of corner points :math:`p_i`, :math:`\\left\\{\\lambda_1 p_1+\\lambda_2 p_2 + \\lambda_3 p_3| \\lambda_i\\ge0\\land\\lambda_1+\\lambda_2+\\lambda_3=1\\right\\}`. Calling :class:`Triangle` with no parameters is equivalent to :class:`Triangle`\ `(p1=[0, 0], p2=[0, 1], p3=[1, 0])`. """ def __init__(self, p1=None, p2=None, p3=None): super().__init__() self._p1 = Point(nd=2) self._p2 = Point(nd=2) self._p3 = Point(nd=2) if p1 is None: p1 = [0, 0] if p2 is None: p2 = [0, 1] if p3 is None: p3 = [1, 0] self.p1 = p1 self.p2 = p2 self.p3 = p3 self.points.extend([self.p1, self.p2, self.p3]) self.fmtstr = "p1={p1!r}, p2={p2!r}, p3={p3!r}" @property def p1(self): """Corner point :math:`p_1=(x_1, y_1)`.""" return self._p1 @p1.setter def p1(self, value): if not isinstance(value, (tuple, list, np.ndarray)) or len(value) != 2: raise TypeError('Expected a 2-element array_like object') self._p1[:] = Point(value) @property def p2(self): """Corner point :math:`p_2=(x_2, y_2)`.""" return self._p2 @p2.setter def p2(self, value): if not isinstance(value, (tuple, list, np.ndarray)) or len(value) != 2: raise TypeError('Expected a 2-element array_like object') self._p2[:] = Point(value) @property def p3(self): """Corner point :math:`p_3=(x_3, y_3)`.""" return self._p3 @p3.setter def p3(self, value): if not isinstance(value, (tuple, list, np.ndarray)) or len(value) != 2: raise TypeError('Expected a 2-element array_like object') self._p3[:] = Point(value) @property def area(self): """:class:`Triangle` area. Computed as: .. math:: A = \\frac{1}{2}|-x_2 y_1 + x_3 y_1 + x_1 y_2 - x_3 y_2 - x_1 y_3 + x_2 y_3| """ x1, y1 = self.p1 x2, y2 = self.p2 x3, y3 = self.p3 return np.abs(-x2 * y1 + x3 * y1 + x1 * y2 - x3 * y2 - x1 * y3 + x2 * y3) / 2 @property def centroid(self): """:class:`Triangle` centroid, :math:`(c_x, c_y)`. Computed as 2D :class:`Point` :math:`(c_x, c_y)` with coordinates: .. math:: c_x = \\frac{x_1 + x_2 + x_3}{3} c_y = \\frac{y_1 + y_2 + y_3}{3} Returns ------- :class:`Point` 2D :class:`Point` of centroid. """ x1, y1 = self.p1 x2, y2 = self.p2 x3, y3 = self.p3 cx = (x1 + x2 + x3) / 3 cy = (y1 + y2 + y3) / 3 return Point([cx, cy])
[docs] def contains(self, point): """Test region membership of `point` in :class:`Triangle`. Parameters ---------- point : array_like Returns ------- :class:`~python:bool` `True` if `point` is within :class:`Triangle`, `False`, otherwise. Notes ----- A point :math:`(p_x, p_y)` is within the bounded region of a triangle with corner points :math:`p_1=(x_1, y_1)`, :math:`p_2=(x_2, y_2)`, and :math:`p_3=(x_3, y_3)`, if the following is true: .. math:: \\frac{(x_1 - x_3) p_y + (x_3 - p_x) y_1 + (p_x - x_1) y_3}{ (y_1-y_2) x_3 + (y_2 - y_3) x_1 + (y_3 - y_1) x_2}\\ge 0\\land \\frac{(x_2 - x_1) p_y + (p_x - x_2) y_1 + (x_1 - p_x) y_2}{ (y_1 - y_2) x_3 + (y_2 - y_3) x_1 + (y_3 - y_1) x_2}\\ge 0\\land \\frac{(x_2 - x_3) p_y + (x_3 - p_x) y_2 + (p_x - x_2) y_3}{ (y_1 - y_2) x_3 + (y_2 - y_3) x_1 + (y_3 - y_1) x_2}\\le 0 """ px, py = Point(point) x1, y1 = self.p1 x2, y2 = self.p2 x3, y3 = self.p3 q1 = ((x1 - x3) * py + (x3 - px) * y1 + (px - x1) * y3) / \ ((y1 - y2) * x3 + (y2 - y3) * x1 + (y3 - y1) * x2) q2 = ((x2 - x1) * py + (px - x2) * y1 + (x1 - px) * y2) / \ ((y1 - y2) * x3 + (y2 - y3) * x1 + (y3 - y1) * x2) q3 = ((x2 - x3) * py + (x3 - px) * y2 + (px - x2) * y3) / \ ((y1 - y2) * x3 + (y2 - y3) * x1 + (y3 - y1) * x2) return q1 >= 0 and q2 >= 0 and q3 <= 0
[docs] def todict(self): """Returns a :class:`~python:dict` of the :class:`Triangle` \ constructor parameters.""" return dict(p1=self.p1, p2=self.p2, p3=self.p3)
[docs]class Ellipse(Geometric2DRegion): """`Geometric2DRegion` for an ellipse. .. versionadded:: 0.3.0 Represents an axis-aligned ellipse centered at :math:`(c_x, c_y)` with semi-axes lengths :math:`r_x, r_y`. Parameters ---------- center : array_like, optional Center of axis-aligned ellipse with semi-axes lengths :math:`r_x, r_y` rx, ry : float Lengths of semi-axes :math:`r_x, r_y` Notes ----- :class:`Ellipse` represents the axis-aligned ellipsoid: .. math:: \\left\\{\\{x, y, z\\}\\in R^3| \\left(\\frac{x-c_x}{r_x}\\right)^2 + \\left(\\frac{y-c_y}{r_y}\\right)^2 + \\left(\\frac{z-c_z}{r_z}\\right)^2\\le 1\\right\\} Calling :class:`Ellipse` with no parameters is equivalent to :class:`Ellipse`\ `(center=[0, 0], rx=1, ry=1)`. """ def __init__(self, center=None, rx=1, ry=1): super().__init__() self._center = Point(nd=2) if center is None: center = [0, 0] self.center = center self.rx = rx self.ry = ry self.points.append(self.center) self.fmtstr = "center={center!r}, rx={rx:.3f}, ry={ry:.3f}" @property def center(self): """:class:`Ellipse` center point :math:`(c_x, c_y)`.""" return self._center @center.setter def center(self, value): if not isinstance(value, (tuple, list, np.ndarray)) or len(value) != 2: raise TypeError('Expected a 2-element array_like object') self._center[:] = Point(value) @property def rx(self): """Length of semi-axis :math:`r_x`.""" return self._rx @rx.setter def rx(self, value): self._rx = float(value) @property def ry(self): """Length of semi-axis :math:`r_y`.""" return self._ry @ry.setter def ry(self, value): self._ry = float(value) @property def area(self): """:class:`Ellipse` area, :math:`A=\\pi r_x r_y`.""" return np.pi * self.rx * self.ry @property def centroid(self): """Alias for :attr:`~Ellipse.center`.""" return self.center
[docs] def contains(self, point): """Test region membership of `point` in :class:`Ellipse`. Parameters ---------- point : array_like Returns ------- :class:`~python:bool` `True` if `point` is within :class:`Ellipse`, `False` otherwise Notes ----- A `point` :math:`(p_x, p_y)` is within the bounded region of an ellipse with center :math:`(c_x, c_y)` and semi-axes lengths :math:`r_x, r_y` if the following is true: .. math:: \\left(\\frac{p_x - c_x}{r_x}\\right)^2 + \\left(\\frac{p_y - c_y}{r_y}\\right)^2\\le 1 """ px, py = Point(point) cx, cy = self.center rx, ry = self.rx, self.ry q1 = (px - cx) ** 2 / rx ** 2 q2 = (py - cy) ** 2 / ry ** 2 return q1 + q2 <= 1
[docs] def todict(self): """Returns a :class:`~python:dict` of the :class:`Ellipse` \ constructor parameters.""" return dict(center=self.center, rx=self.rx, ry=self.ry)
[docs]class Circle(Geometric2DRegion): """`Geometric2DRegion` for a circle. .. versionadded:: 0.3.0 Represents the bounded region with center :math:`(h, k)` and radius :math:`r`. Parameters ---------- center : array_like, optional Center point :math:`(h, k)` of circle. r : float, optional Radius :math:`r` of circle. Notes ----- Calling :class:`Circle` with no parameters is equivalent to :class:`Circle`\ `(center=[0, 0], r=1.0)`. """ def __init__(self, center=None, r=1.0): super().__init__() self._center = Point(nd=2) if center is None: center = [0, 0] self.center = center self.r = r self.points.append(self.center) self.fmtstr = "center={center!r}, r={r:.3f}" @property def center(self): """Center point :math:`(h, k)` of circle.""" return self._center @center.setter def center(self, value): if not isinstance(value, (tuple, list, np.ndarray)) or len(value) != 2: raise TypeError('Expected a 2-element array_like object') self._center[:] = Point(value) @property def r(self): """:class:`Circle` radius, :math:`r`.""" return self._r @r.setter def r(self, value): self._r = float(value) @property def area(self): """:class:`Circle` area, :math:`A=\\pi r^2`.""" return np.pi * self.r ** 2 @property def centroid(self): """Alias for :attr:`~Circle.center`.""" return self.center
[docs] def contains(self, point): """Test region membership of `point` in :class:`Circle`. Parameters ---------- point : array_like Returns ------- :class:`~python:bool` `True` if `point` is within :class:`Circle`, `False` otherwise. Notes ----- A `point` :math:`(p_x, p_y)` is within the bounded region of a circle with center :math:`(h, k)` and radius :math:`r` if the following is true: .. math:: (p_x - h)^2 + (p_y - k)^2 \\le r^2 """ x, y = Point(point) h, k = self.center r = self.r return (x - h) ** 2 + (y - k) ** 2 <= r ** 2
[docs] def todict(self): """Returns a :class:`~python:dict` of the :class:`Circle` \ constructor parameters.""" return dict(center=self.center, r=self.r)