Source code for skspatial.objects.sphere

"""Module for the Sphere class."""

from __future__ import annotations

import math
from typing import Tuple

import numpy as np
from mpl_toolkits.mplot3d import Axes3D

from skspatial._functions import np_float
from skspatial.objects._base_sphere import _BaseSphere
from skspatial.objects._mixins import _ToPointsMixin
from skspatial.objects.line import Line
from skspatial.objects.point import Point
from skspatial.objects.points import Points
from skspatial.objects.vector import Vector
from skspatial.typing import array_like


class Sphere(_BaseSphere, _ToPointsMixin):
    """
    A sphere in 3D space.

    The sphere is defined by a 3D point and a radius.

    Parameters
    ----------
    point : (3,) array_like
        Center of the sphere.
    radius : {int, float}
        Radius of the sphere.

    Attributes
    ----------
    point : (3,) Point
        Center of the sphere.
    radius : {int, float}
        Radius of the sphere.
    dimension : int
        Dimension of the sphere.

    Raises
    ------
    ValueError
        If the radius is not positive.
        If the point is not 3D.

    Examples
    --------
    >>> from skspatial.objects import Sphere

    >>> sphere = Sphere([1, 2, 3], 5)

    >>> sphere
    Sphere(point=Point([1, 2, 3]), radius=5)

    >>> sphere.dimension
    3

    >>> sphere.surface_area().round(2)
    np.float64(314.16)

    >>> Sphere([0, 0], 0)
    Traceback (most recent call last):
    ...
    ValueError: The radius must be positive.

    >>> Sphere([0, 0, 0, 0], 1)
    Traceback (most recent call last):
    ...
    ValueError: The point must be 3D.

    """

    def __init__(self, point: array_like, radius: float):
        super().__init__(point, radius)

        if self.point.dimension != 3:
            raise ValueError("The point must be 3D.")

[docs] @np_float def surface_area(self) -> float: r""" Return the surface area of the sphere. The surface area :math:`A` of a sphere with radius :math:`r` is .. math:: A = 4 \pi r ^ 2 Returns ------- np.float64 Surface area of the sphere. Examples -------- >>> from skspatial.objects import Sphere >>> Sphere([0, 0, 0], 1).surface_area().round(2) np.float64(12.57) >>> Sphere([0, 0, 0], 2).surface_area().round(2) np.float64(50.27) """ return 4 * np.pi * self.radius**2
[docs] @np_float def volume(self) -> float: r""" Return the volume of the sphere. The volume :math:`V` of a sphere with radius :math:`r` is .. math:: V = \frac{4}{3} \pi r ^ 3 Returns ------- np.float64 Volume of the sphere. Examples -------- >>> from skspatial.objects import Sphere >>> Sphere([0, 0, 0], 1).volume().round(2) np.float64(4.19) >>> Sphere([0, 0, 0], 2).volume().round(2) np.float64(33.51) """ return 4 / 3 * np.pi * self.radius**3
[docs] def intersect_line(self, line: Line) -> Tuple[Point, Point]: """ Intersect the sphere with a line. A line intersects a sphere at two points. Parameters ---------- line : Line Input line. Returns ------- point_a, point_b : Point The two points of intersection. Examples -------- >>> from skspatial.objects import Sphere, Line >>> sphere = Sphere([0, 0, 0], 1) >>> sphere.intersect_line(Line([0, 0, 0], [1, 0, 0])) (Point([-1., 0., 0.]), Point([1., 0., 0.])) >>> sphere.intersect_line(Line([0, 0, 1], [1, 0, 0])) (Point([0., 0., 1.]), Point([0., 0., 1.])) >>> sphere.intersect_line(Line([0, 0, 2], [1, 0, 0])) Traceback (most recent call last): ... ValueError: The line does not intersect the sphere. """ vector_to_line = Vector.from_points(self.point, line.point) vector_unit = line.direction.unit() dot = vector_unit.dot(vector_to_line) discriminant = dot**2 - (vector_to_line.norm() ** 2 - self.radius**2) if discriminant < 0: raise ValueError("The line does not intersect the sphere.") pm = np.array([-1, 1]) # Array to compute minus/plus. distances = -dot + pm * math.sqrt(discriminant) point_a, point_b = line.point + distances.reshape(-1, 1) * vector_unit return Point(point_a), Point(point_b)
[docs] @classmethod def best_fit(cls, points: array_like) -> Sphere: """ Return the sphere of best fit for a set of 3D points. Parameters ---------- points : array_like Input 3D points. Returns ------- Sphere The sphere of best fit. Raises ------ ValueError If the points are not 3D. If there are fewer than four points. If the points lie in a plane. Examples -------- >>> import numpy as np >>> from skspatial.objects import Sphere >>> points = [[1, 0, 1], [0, 1, 1], [1, 2, 1], [1, 1, 2]] >>> sphere = Sphere.best_fit(points) >>> sphere.point Point([1., 1., 1.]) >>> np.round(sphere.radius, 2) np.float64(1.0) """ points = Points(points) if points.dimension != 3: raise ValueError("The points must be 3D.") if points.shape[0] < 4: raise ValueError("There must be at least 4 points.") if points.affine_rank() != 3: raise ValueError("The points must not be in a plane.") n = points.shape[0] A = np.hstack((2 * points, np.ones((n, 1)))) b = (points**2).sum(axis=1) c, _, _, _ = np.linalg.lstsq(A, b, rcond=None) center = c[:3] radius = float(np.sqrt(np.dot(center, center) + c[3])) return cls(center, radius)
[docs] def to_mesh(self, n_angles: int = 30) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Return coordinate matrices for the 3D surface of the sphere. Parameters ---------- n_angles: int Number of angles used to generate the coordinate matrices. Returns ------- X, Y, Z: (n_angles, n_angles) ndarray Coordinate matrices. Examples -------- >>> from skspatial.objects import Sphere >>> X, Y, Z = Sphere([0, 0, 0], 1).to_mesh(5) >>> X.round(3) array([[ 0. , 0. , 0. , 0. , 0. ], [ 0. , 0.707, 0. , -0.707, -0. ], [ 0. , 1. , 0. , -1. , -0. ], [ 0. , 0.707, 0. , -0.707, -0. ], [ 0. , 0. , 0. , -0. , -0. ]]) >>> Y.round(3) array([[ 0. , 0. , 0. , 0. , 0. ], [ 0.707, 0. , -0.707, -0. , 0.707], [ 1. , 0. , -1. , -0. , 1. ], [ 0.707, 0. , -0.707, -0. , 0.707], [ 0. , 0. , -0. , -0. , 0. ]]) >>> Z.round(3) array([[ 1. , 1. , 1. , 1. , 1. ], [ 0.707, 0.707, 0.707, 0.707, 0.707], [ 0. , 0. , 0. , 0. , 0. ], [-0.707, -0.707, -0.707, -0.707, -0.707], [-1. , -1. , -1. , -1. , -1. ]]) """ angles_a = np.linspace(0, np.pi, n_angles) angles_b = np.linspace(0, 2 * np.pi, n_angles) sin_angles_a = np.sin(angles_a) cos_angles_a = np.cos(angles_a) sin_angles_b = np.sin(angles_b) cos_angles_b = np.cos(angles_b) X = self.point[0] + self.radius * np.outer(sin_angles_a, sin_angles_b) Y = self.point[1] + self.radius * np.outer(sin_angles_a, cos_angles_b) Z = self.point[2] + self.radius * np.outer(cos_angles_a, np.ones_like(angles_b)) return X, Y, Z
[docs] def plot_3d(self, ax_3d: Axes3D, n_angles: int = 30, **kwargs) -> None: """ Plot the sphere in 3D. Parameters ---------- ax_3d : Axes3D Instance of :class:`~mpl_toolkits.mplot3d.axes3d.Axes3D`. kwargs : dict, optional Additional keywords passed to :meth:`~mpl_toolkits.mplot3d.axes3d.Axes3D.plot_surface`. Examples -------- .. plot:: :include-source: >>> import matplotlib.pyplot as plt >>> from mpl_toolkits.mplot3d import Axes3D >>> from skspatial.objects import Sphere >>> fig = plt.figure() >>> ax = fig.add_subplot(111, projection='3d') >>> sphere = Sphere([1, 2, 3], 2) >>> sphere.plot_3d(ax, alpha=0.2) >>> sphere.point.plot_3d(ax, s=100) """ X, Y, Z = self.to_mesh(n_angles) ax_3d.plot_surface(X, Y, Z, **kwargs)