# -*- coding: utf-8 -*-
"""
=======================================================================
3D geometric shapes (:mod:`sknano.utils.geometric_shapes._3D_shapes`)
=======================================================================
.. currentmodule:: sknano.utils.geometric_shapes._3D_shapes
"""
from __future__ import absolute_import, division, print_function
import six
__docformat__ = 'restructuredtext en'
from abc import ABCMeta, abstractproperty
import numbers
import numpy as np
from sknano.core.math import Point, Vector, vector as vec
from ._base import GeometricRegion, GeometricTransformsMixin
__all__ = ['Geometric3DRegion', 'Parallelepiped', 'Cuboid', 'Cube',
'Ellipsoid', 'Spheroid', 'Sphere']
[docs]class Geometric3DRegion(six.with_metaclass(ABCMeta, GeometricTransformsMixin,
GeometricRegion)):
"""Abstract base class for representing 3D geometric regions."""
@abstractproperty
def volume(self):
"""Volume of 3D geometric region."""
raise NotImplementedError
[docs]class Parallelepiped(Geometric3DRegion):
"""Abstract representation of parallelepiped.
.. versionadded:: 0.3.0
Represents a parallelepiped with origin :math:`o` and directions
:math:`u, v, w`.
Parameters
----------
o : array_like, optional
parallelepiped origin
u, v, w : array_like, optional
parallelepiped direction vectors stemming from origin `o`.
"""
def __init__(self, o=None, u=None, v=None, w=None):
super(Parallelepiped, self).__init__()
if o is None:
o = Point(nd=3)
elif isinstance(o, (tuple, list, np.ndarray)):
o = Point(o)
self._o = o
if u is None:
u = [1., 0., 0.]
self._u = Vector(u, p0=self._o)
if v is None:
v = [1., 1., 0.]
self._v = Vector(v, p0=self._o)
if w is None:
w = [0., 1., 1.]
self._w = Vector(w, p0=self._o)
self.points.append(self.o)
self.vectors.extend([self.u, self.v, self.w])
def __repr__(self):
return "Parallelepiped(o={!r}, u={!r}, v={!r}, w={!r})".format(
self.o.tolist(), self.u.tolist(), self.v.tolist(), self.w.tolist())
@property
def o(self):
return self._o
@o.setter
def o(self, value):
self._o[:] = value
@property
def u(self):
return self._u
@u.setter
def u(self, value):
self._u[:] = value
@property
def v(self):
return self._v
@v.setter
def v(self, value):
self._v[:] = value
@property
def w(self):
return self._w
@w.setter
def w(self, value):
self._w[:] = value
@property
def center(self):
return self.centroid
@property
def volume(self):
u = self.u
v = self.v
w = self.w
return vec.scalar_triple_product(u, v, w)
@property
def centroid(self):
o = self.o
u = self.u
v = self.v
w = self.w
xcom = 0.5 * (2 * o.x + u.x + v.x + w.x)
ycom = 0.5 * (2 * o.y + u.y + v.y + w.y)
zcom = 0.5 * (2 * o.y + u.y + v.y + w.z)
return Point([xcom, ycom, zcom])
[docs] def contains_point(self, point):
"""Check if point is contained within volume of `Parallelepiped`."""
p = Point(point)
o = self.o
u = self.u
v = self.v
w = self.w
d1 = (v.z * (w.x * (p.y - o.y) + w.y * (o.x - p.x)) +
w.z * (v.x * (o.y - p.y) + v.y * (p.x - o.x)) +
o.z * (v.y * w.x - v.x * w.y) +
p.z * (v.x * w.y - v.y * w.x)) / \
(u.z * (v.x * w.y - v.y * w.x) +
u.y * (v.z * w.x - v.x * w.z) +
u.x * (v.y * w.z - v.z * w.y))
d2 = (u.z * (w.x * (p.y - o.y) + w.y * (o.x - p.x)) +
w.z * (u.x * (o.y - p.y) + u.y * (p.x - o.x)) +
o.z * (u.y * w.x - u.x * w.y) +
p.z * (u.x * w.y - u.y * w.x)) / \
(u.z * (v.y * w.x - v.x * w.y) +
u.y * (v.x * w.z - v.z * w.x) +
u.x * (v.z * w.y - v.y * w.z))
d3 = (u.z * (v.x * (p.y - o.y) + v.y * (o.x - p.x)) +
v.z * (u.x * (o.y - p.y) + u.y * (p.x - o.x)) +
o.z * (u.y * v.x - u.x * v.y) +
p.z * (u.x * v.y - u.y * v.x)) / \
(u.z * (v.x * w.y - v.y * w.x) +
u.y * (v.z * w.x - v.x * w.z) +
u.x * (v.y * w.z - v.z * w.y))
return d1 >= 0 and d1 <= 1 and d2 >= 0 and d2 <= 1 and \
d3 >= 0 and d3 <= 1
[docs] def update_region_limits(self):
self.limits['x']['min'] = self.xmin
self.limits['x']['max'] = self.xmax
self.limits['y']['min'] = self.ymin
self.limits['y']['max'] = self.ymax
self.limits['z']['min'] = self.zmin
self.limits['z']['max'] = self.zmax
[docs]class Cuboid(Geometric3DRegion):
"""Abstract representation of cuboid.
.. versionadded:: 0.3.0
Parameters
----------
pmin, pmax : sequence, optional
xmin, ymin, zmin : float, optional
xmax, ymax, zmax : float, optional
"""
def __init__(self, pmin=None, pmax=None,
xmin=None, ymin=None, zmin=None,
xmax=None, ymax=None, zmax=None):
super(Cuboid, self).__init__()
if pmin is None:
pmin = Point([xmin, ymin, zmin])
elif isinstance(pmin, (tuple, list, np.ndarray)):
pmin = Point(pmin)
self._pmin = pmin
if pmax is None:
pmax = Point([xmax, ymax, zmax])
elif isinstance(pmax, (tuple, list, np.ndarray)):
pmax = Point(pmax)
self._pmax = pmax
self.points.extend([self.pmin, self.pmax])
def __repr__(self):
return "Cuboid(pmin={!r}, pmax={!r})".format(
self.pmin.tolist(), self.pmax.tolist())
@property
def pmin(self):
return self._pmin
@pmin.setter
def pmin(self, point):
if not isinstance(point, (list, np.ndarray)) or \
len(point) != self.pmin.nd:
raise TypeError('Expected a 3-element list or ndarray')
self._pmin[:] = point
@property
def pmax(self):
return self._pmax
@pmax.setter
def pmax(self, point):
if not isinstance(point, (list, np.ndarray)) or \
len(point) != self.pmax.nd:
raise TypeError('Expected a 3-element list or ndarray')
self._pmax[:] = point
@property
def xmin(self):
return self.pmin.x
@xmin.setter
def xmin(self, value):
if not isinstance(value, numbers.Number):
raise TypeError('Expected a number')
self._pmin.x = value
@property
def ymin(self):
return self.pmin.y
@ymin.setter
def ymin(self, value):
if not isinstance(value, numbers.Number):
raise TypeError('Expected a number')
self._pmin.y = value
@property
def zmin(self):
return self.pmin.z
@zmin.setter
def zmin(self, value):
if not isinstance(value, numbers.Number):
raise TypeError('Expected a number')
self._pmin.z = value
@property
def xmax(self):
return self.pmax.x
@xmax.setter
def xmax(self, value):
if not isinstance(value, numbers.Number):
raise TypeError('Expected a number')
self._pmax.x = value
@property
def ymax(self):
return self.pmax.y
@ymax.setter
def ymax(self, value):
if not isinstance(value, numbers.Number):
raise TypeError('Expected a number')
self._pmax.y = value
@property
def zmax(self):
return self.pmax.z
@zmax.setter
def zmax(self, value):
if not isinstance(value, numbers.Number):
raise TypeError('Expected a number')
self._pmax.z = value
@property
def center(self):
return self.centroid
@property
def centroid(self):
xcom = 0.5 * (self.xmin + self.xmax)
ycom = 0.5 * (self.ymin + self.ymax)
zcom = 0.5 * (self.zmin + self.zmax)
return Point([xcom, ycom, zcom])
@property
def a(self):
return self.xmax - self.xmin
@property
def b(self):
return self.ymax - self.ymin
@property
def c(self):
return self.zmax - self.zmin
@property
def volume(self):
a = self.a
b = self.b
c = self.c
return a * b * c
[docs] def contains_point(self, point):
"""Check if point is contained within volume of `Cuboid`."""
p = Point(point)
return (p.x >= self.xmin) and (p.x <= self.xmax) and \
(p.y >= self.ymin) and (p.y <= self.ymax) and \
(p.z >= self.zmin) and (p.z <= self.zmax)
[docs] def update_region_limits(self):
self.limits['x']['min'] = self.xmin
self.limits['x']['max'] = self.xmax
self.limits['y']['min'] = self.ymin
self.limits['y']['max'] = self.ymax
self.limits['z']['min'] = self.zmin
self.limits['z']['max'] = self.zmax
[docs]class Cube(Geometric3DRegion):
"""Abstract data structure representing a cube.
.. versionadded:: 0.3.0
Parameters
----------
center : sequence, optional
Either a 3-tuple of floats or an instance of the
:class:`~sknano.core.Point` class specifying the :math:`(x,y,z)`
coordinates of the cube center.
a : float, optional
length of edge
"""
def __init__(self, center=None, a=None):
super(Cube, self).__init__()
if center is None:
center = Point()
elif isinstance(center, (tuple, list, np.ndarray)):
center = Point(center)
self._center = center
if a is None:
a = 1.0
self._a = a
self.points.append(self.center)
def __repr__(self):
return("Cube(center={!r}, a={!r})".format(self.center.tolist(),
self.a))
@property
def center(self):
return self._center
@property
def a(self):
return self._a
@property
def centroid(self):
self.center
@property
def volume(self):
a = self.a
return a**3
[docs] def contains_point(self, point):
p = Point(point)
c = self.center
a = self.a
xmin = c.x - a / 2
ymin = c.y - a / 2
zmin = c.z - a / 2
xmax = c.x + a / 2
ymax = c.y + a / 2
zmax = c.z + a / 2
return (p.x >= xmin) and (p.x <= xmax) and \
(p.y >= ymin) and (p.y <= ymax) and \
(p.z >= zmin) and (p.z <= zmax)
[docs] def update_region_limits(self):
self.limits['x']['min'] = self.xmin
self.limits['x']['max'] = self.xmax
self.limits['y']['min'] = self.ymin
self.limits['y']['max'] = self.ymax
self.limits['z']['min'] = self.zmin
self.limits['z']['max'] = self.zmax
[docs]class Ellipsoid(Geometric3DRegion):
"""Abstract data structure representing an ellipsoid.
.. versionadded:: 0.3.0
The general ellipsoid is a quadratic surface with is given in
Cartesian coordinates by:
.. math::
\\frac{x^2}{a^2} + \\frac{y^2}{b^2} + \\frac{z^2}{c^2} = 1
Parameters
----------
center : sequence or :class:`~sknano.core.Point`
Either a 3-tuple of floats or an instance of the
:class:`~sknano.core.Point` class specifying the :math:`(x,y,z)`
coordinates of the :class:`Ellipsoid` center.
a, b, c : float, optional
Semi-principal axes :math:`a, b, c` of axis-aligned
:class:`Ellipsoid`
"""
def __init__(self, center=None, a=None, b=None, c=None):
super(Ellipsoid, self).__init__()
if center is None:
center = Point()
elif isinstance(center, (tuple, list, np.ndarray)):
center = Point(center)
self._center = center
if a is None:
a = 0.5
if b is None:
b = 0.5
if c is None:
c = 0.5
self._a, self._b, self._c = a, b, c
self.points.append(self.center)
def __repr__(self):
return("Ellipsoid(center={!r}, a={!r}, b={!r}, c={!r})".format(
self.center, self.a, self.b, self.c))
@property
def center(self):
return self._center
@property
def a(self):
return self._a
@property
def b(self):
return self._b
@property
def c(self):
return self._c
@property
def centroid(self):
return self.center
@property
def volume(self):
a = self.a
b = self.b
c = self.c
return 4 / 3 * np.pi * a * b * c
[docs] def contains_point(self, point):
"""Check if point is contained within volume of :class:`Ellipsoid`."""
p = Point(point)
c = self.center
a, b, c = self.a, self.b, self.c
return (p.x - c.x)**2 / a**2 + (p.y - c.y)**2 / b**2 + \
(p.z - c.z)**2 / c**2 <= 1.0
[docs] def update_region_limits(self):
c = self.center
r = self.r
self.limits['x']['min'] = c.x - r
self.limits['x']['max'] = c.x + r
self.limits['y']['min'] = c.y - r
self.limits['y']['max'] = c.y + r
self.limits['z']['min'] = c.z - r
self.limits['z']['max'] = c.z + r
[docs]class Spheroid(Geometric3DRegion):
"""Abstract data structure representing a spheroid.
.. versionadded:: 0.3.0
The general spheroid is a quadratic surface with is given in
Cartesian coordinates by:
.. math::
\\frac{x^2 + y^2}{a^2} + \\frac{z^2}{c^2} = 1
Parameters
----------
center : sequence, optional
Either a 3-tuple of floats or an instance of the
:class:`~sknano.core.Point` class specifying the :math:`(x,y,z)`
coordinates of the :class:`Spheroid` center.
a, c : float, optional
Semi-axes :math:`a, c` of axis-aligned
:class:`Spheroid` with symmetry axis along :math:`z` axis.
"""
def __init__(self, center=None, a=None, c=None):
super(Spheroid, self).__init__()
if center is None:
center = Point()
elif isinstance(center, (tuple, list, np.ndarray)):
center = Point(center)
self._center = center
if a is None:
a = 0.5
if c is None:
c = 0.5
self._a, self._c = a, c
self.points.append(self.center)
def __repr__(self):
return("Spheroid(center={!r}, a={!r}, c={!r})".format(
self.center, self.a, self.c))
@property
def center(self):
return self._center
@property
def a(self):
return self._a
@property
def c(self):
return self._c
@property
def centroid(self):
return self.center
@property
def volume(self):
a = self.a
c = self.c
return 4 / 3 * np.pi * a**2 * c
[docs] def contains_point(self, point=None):
"""Check if point is contained within volume of :class:`Spheroid`."""
x, y, z = point
h, k, l = self.center
a, c = self.a, self.c
return ((x - h)**2 + (y - k)**2) / a**2 + (z - l)**2 / c**2 <= 1.0
[docs] def update_region_limits(self):
c = self.center
r = self.r
self.limits['x']['min'] = c.x - r
self.limits['x']['max'] = c.x + r
self.limits['y']['min'] = c.y - r
self.limits['y']['max'] = c.y + r
self.limits['z']['min'] = c.z - r
self.limits['z']['max'] = c.z + r
[docs]class Sphere(Geometric3DRegion):
"""Abstract data structure representing a sphere.
.. versionadded:: 0.3.0
Parameters
----------
center : sequence, optional
Either a 3-tuple of floats or an instance of the
:class:`~sknano.core.Point` class specifying the :math:`(x,y,z)`
coordinates of the :class:`Sphere` center.
r : float, optional
Sphere radius :math:`r`
"""
def __init__(self, center=None, r=None):
super(Sphere, self).__init__()
if center is None:
center = Point()
elif isinstance(center, (tuple, list, np.ndarray)):
center = Point(center)
self._center = center
if r is None:
r = 1.0
self._r = r
self.points.append(self.center)
def __repr__(self):
return("Sphere(center={!r}, r={!r})".format(self.center, self.r))
@property
def r(self):
return self._r
@r.setter
def r(self, value):
self._r = value
@property
def center(self):
return self._center
@property
def centroid(self):
return self.center
@property
def volume(self):
r = self.r
return 4 / 3 * np.pi * r**3
[docs] def contains_point(self, point):
p = point
c = self.center
r = self.r
return (p.x - c.x)**2 + (p.y - c.y)**2 + (p.z - c.z)**2 <= r**2
[docs] def update_region_limits(self):
c = self.center
r = self.r
self.limits['x']['min'] = c.x - r
self.limits['x']['max'] = c.x + r
self.limits['y']['min'] = c.y - r
self.limits['y']['max'] = c.y + r
self.limits['z']['min'] = c.z - r
self.limits['z']['max'] = c.z + r