def __init__(self, point: array_like, vector: array_like, error = None):

        self.point = Point(point)
        self.vector = Vector(vector)
        self.error = error

        if self.point.dimension != self.vector.dimension:
            raise ValueError("The point and vector must have the same dimension.")

        if self.vector.is_zero(rel_tol=0, abs_tol=0):
            raise ValueError("The vector must not be the zero vector.")

        self.dimension = self.point.dimension
Exemplo n.º 2
0
    def __init__(self, point: array_like, vector: array_like, **kwargs):

        self.point = Point(point)
        self.vector = Vector(vector)

        if self.point.dimension != self.vector.dimension:
            raise ValueError(
                "The point and vector must have the same dimension.")

        if self.vector.is_zero(**kwargs):
            raise ValueError("The vector must not be the zero vector.")

        self.dimension = self.point.dimension
Exemplo n.º 3
0
    def from_points(cls, point_a: array_like, point_b: array_like,
                    point_c: array_like, **kwargs) -> Plane:
        """
        Instantiate a plane from three points.

        The three points lie on the plane.

        Parameters
        ----------
        point_a, point_b, point_c: array_like
            Three points defining the plane.
        kwargs: dict, optional
            Additional keywords passed to :meth:`Points.are_collinear`.

        Returns
        -------
        Plane
            Plane containing the three input points.

        Raises
        ------
        ValueError
            If the points are collinear.

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

        >>> Plane.from_points([0, 0], [1, 0], [3, 3])
        Plane(point=Point([0, 0, 0]), normal=Vector([0, 0, 3]))

        The order of the points affects the direction of the normal vector.

        >>> Plane.from_points([0, 0], [3, 3], [1, 0])
        Plane(point=Point([0, 0, 0]), normal=Vector([ 0,  0, -3]))

        >>> Plane.from_points([0, 0], [0, 1], [0, 3])
        Traceback (most recent call last):
        ...
        ValueError: The points must not be collinear.

        """
        if Points([point_a, point_b, point_c]).are_collinear(**kwargs):
            raise ValueError("The points must not be collinear.")

        vector_ab = Vector.from_points(point_a, point_b)
        vector_ac = Vector.from_points(point_a, point_c)

        return Plane.from_vectors(point_a, vector_ab, vector_ac)
Exemplo n.º 4
0
    def project_point(self, point: array_like) -> Point:
        """
        Project a point onto the line.

        Parameters
        ----------
        point : array_like
            Input point.

        Returns
        -------
        Point
            Projection of the point onto the line.

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

        >>> Line(point=[0, 0], direction=[8, 0]).project_point([5, 5])
        Point([5., 0.])

        >>> Line(point=[0, 0, 0], direction=[1, 1, 0]).project_point([5, 5, 3])
        Point([5., 5., 0.])

        """
        # Vector from the point on the line to the point in space.
        vector_to_point = Vector.from_points(self.point, point)

        # Project the vector onto the line.
        vector_projected = self.direction.project_vector(vector_to_point)

        # Add the projected vector to the point on the line.
        return self.point + vector_projected
Exemplo n.º 5
0
    def intersect_line(self, line: Line, **kwargs) -> Point:
        """
        Intersect the plane with a line.

        The line and plane must not be parallel.

        Parameters
        ----------
        line : Line
            Input line.
        kwargs : dict, optional
            Additional keywords passed to :meth:`Vector.is_perpendicular`.

        Returns
        -------
        Point
            The point of intersection.

        Raises
        ------
        ValueError
            If the line and plane are parallel.

        References
        ----------
        http://geomalgorithms.com/a05-_intersect-1.html

        Examples
        --------
        >>> from skspatial.objects import Line, Plane

        >>> line = Line([0, 0, 0], [0, 0, 1])
        >>> plane = Plane([0, 0, 0], [0, 0, 1])

        >>> plane.intersect_line(line)
        Point([0., 0., 0.])

        >>> plane = Plane([2, -53, -7], [0, 0, 1])
        >>> plane.intersect_line(line)
        Point([ 0.,  0., -7.])

        >>> line = Line([0, 1, 0], [1, 0, 0])
        >>> plane.intersect_line(line)
        Traceback (most recent call last):
        ...
        ValueError: The line and plane must not be parallel.

        """
        if self.normal.is_perpendicular(line.direction, **kwargs):
            raise ValueError("The line and plane must not be parallel.")

        vector_plane_line = Vector.from_points(self.point, line.point)

        num = -self.normal.dot(vector_plane_line)
        denom = self.normal.dot(line.direction)

        # Vector along the line to the intersection point.
        vector_line_scaled = num / denom * line.direction

        return line.point + vector_line_scaled
Exemplo n.º 6
0
    def from_points(cls, point_a: array_like, point_b: array_like) -> 'Line':
        """
        Instantiate a line from two points.

        Parameters
        ----------
        point_a, point_b : array_like
            Two points defining the line.

        Returns
        -------
        Line
            Line containing the two input points.

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

        >>> Line.from_points([0, 0], [1, 0])
        Line(point=Point([0, 0]), direction=Vector([1, 0]))

        The order of the points affects the line point and direction vector.

        >>> Line.from_points([1, 0], [0, 0])
        Line(point=Point([1, 0]), direction=Vector([-1,  0]))

        """
        vector_ab = Vector.from_points(point_a, point_b)

        return cls(point_a, vector_ab)
Exemplo n.º 7
0
    def distance_point(self, other: array_like) -> np.float64:
        """
        Return the distance to another point.

        Parameters
        ----------
        other : array_like
            Other point.

        Returns
        -------
        np.float64
            Distance between the points.

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

        >>> point = Point([1, 2])
        >>> point.distance_point([1, 2])
        0.0

        >>> point.distance_point([-1, 2])
        2.0

        >>> Point([1, 2, 0]).distance_point([1, 2, 3])
        3.0

        """
        vector = Vector.from_points(self, other)

        return vector.norm()
Exemplo n.º 8
0
def _intersect_line_with_infinite_cylinder(
    cylinder: Cylinder,
    line: Line,
    n_digits: Optional[int],
) -> Tuple[Point, Point]:

    p_c = cylinder.point
    v_c = cylinder.vector.unit()
    r = cylinder.radius

    p_l = line.point
    v_l = line.vector.unit()

    delta_p = Vector.from_points(p_c, p_l)

    a = (v_l - v_l.dot(v_c) * v_c).norm() ** 2
    b = 2 * (v_l - v_l.dot(v_c) * v_c).dot(delta_p - delta_p.dot(v_c) * v_c)
    c = (delta_p - delta_p.dot(v_c) * v_c).norm() ** 2 - r**2

    try:
        X = _solve_quadratic(a, b, c, n_digits=n_digits)
    except ValueError:
        raise ValueError("The line does not intersect the cylinder.")

    point_a, point_b = p_l + X.reshape(-1, 1) * v_l

    return point_a, point_b
Exemplo n.º 9
0
    def project_point(self, point: array_like) -> Point:
        """
        Project a point onto the plane.

        Parameters
        ----------
        point : array_like
            Input point.

        Returns
        -------
        Point
            Projection of the point onto the plane.

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

        >>> plane = Plane(point=[0, 0, 0], normal=[0, 0, 2])

        >>> plane.project_point([10, 2, 5])
        Point([10.,  2.,  0.])

        >>> plane.project_point([5, 9, -3])
        Point([5., 9., 0.])

        """
        # Vector from the point in space to the point on the plane.
        vector_to_plane = Vector.from_points(point, self.point)

        # Perpendicular vector from the point in space to the plane.
        vector_projected = self.normal.project_vector(vector_to_plane)

        return Point(point) + vector_projected
Exemplo n.º 10
0
    def project_vector(self, vector: array_like) -> Vector:
        """
        Project a vector onto the plane.

        Parameters
        ----------
        vector : array_like
            Input vector.

        Returns
        -------
        Vector
            Projection of the vector onto the plane.

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

        >>> plane = Plane([0, 4, 0], [0, 1, 1])

        >>> plane.project_vector([2, 4, 8])
        Vector([ 2., -2.,  2.])

        """
        point_in_space = self.point + vector
        point_on_plane = self.project_point(point_in_space)

        return Vector.from_points(self.point, point_on_plane)
Exemplo n.º 11
0
    def from_points(cls, point_a: array_like, point_b: array_like, radius: float) -> Cylinder:
        """
        Instantiate a cylinder from two points and a radius.

        Parameters
        ----------
        point_a, point_b : array_like
            The centres of the two circular ends.
        radius : float
            The cylinder radius.

        Returns
        -------
        Cylinder
            The cylinder defined by the two points and the radius.

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

        >>> Cylinder.from_points([0, 0, 0], [0, 0, 1], 1)
        Cylinder(point=Point([0, 0, 0]), vector=Vector([0, 0, 1]), radius=1)

        >>> Cylinder.from_points([0, 0, 0], [0, 0, 2], 1)
        Cylinder(point=Point([0, 0, 0]), vector=Vector([0, 0, 2]), radius=1)

        """
        vector_ab = Vector.from_points(point_a, point_b)

        return cls(point_a, vector_ab, radius)
Exemplo n.º 12
0
    def __init__(self, point: array_like, vector: array_like, radius: float):

        self.point = Point(point)
        self.vector = Vector(vector)

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

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

        if self.vector.is_zero():
            raise ValueError("The vector must not be the zero vector.")

        if not radius > 0:
            raise ValueError("The radius must be positive.")

        self.radius = radius

        self.dimension = self.point.dimension
Exemplo n.º 13
0
    def best_fit(cls, points: array_like) -> Plane:
        """
        Return the plane of best fit for a set of 3D points.

        Parameters
        ----------
        points : array_like
             Input 3D points.

        Returns
        -------
        Plane
            The plane of best fit.

        Raises
        ------
        ValueError
            If the points are collinear or are not 3D.

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

        >>> points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]
        >>> plane = Plane.best_fit(points)

        The point on the plane is the centroid of the points.

        >>> plane.point
        Point([0.25, 0.25, 0.25])

        The plane normal is a unit vector.

        >>> plane.normal.round(3)
        Vector([-0.577, -0.577, -0.577])

        >>> Plane.best_fit([[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]])
        Plane(point=Point([0.5, 0.5, 0. ]), normal=Vector([0., 0., 1.]))

        """
        points = Points(points)

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

        if points.are_collinear(tol=0):
            raise ValueError("The points must not be collinear.")

        points_centered, centroid = points.mean_center(return_centroid=True)

        u, _, _ = np.linalg.svd(points_centered.T)
        normal = Vector(u[:, -1])

        return cls(centroid, normal)
Exemplo n.º 14
0
    def from_vectors(cls, point: array_like, vector_a: array_like,
                     vector_b: array_like, **kwargs) -> Plane:
        """
        Instantiate a plane from a point and two vectors.

        The two vectors span the plane.

        Parameters
        ----------
        point : array_like
            Point on the plane.
        vector_a, vector_b : array_like
            Input vectors.
        kwargs : dict, optional
            Additional keywords passed to :meth:`Vector.is_parallel`.

        Returns
        -------
        Plane
            Plane containing input point and spanned by the two input vectors.

        Raises
        ------
        ValueError
            If the vectors are parallel.

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

        >>> Plane.from_vectors([0, 0], [1, 0], [0, 1])
        Plane(point=Point([0, 0, 0]), normal=Vector([0, 0, 1]))

        >>> Plane.from_vectors([0, 0], [1, 0], [2, 0])
        Traceback (most recent call last):
        ...
        ValueError: The vectors must not be parallel.

        """
        vector_a = Vector(vector_a)

        if vector_a.is_parallel(vector_b, **kwargs):
            raise ValueError("The vectors must not be parallel.")

        # The cross product returns a 3D vector.
        vector_normal = vector_a.cross(vector_b)

        # Convert the point to 3D so that it matches the vector dimension.
        point = Point(point).set_dimension(3)

        return cls(point, vector_normal)
Exemplo n.º 15
0
    def normal(self) -> Vector:
        r"""
        Return a vector normal to the triangle.

        The normal vector is calculated as

        .. math::
            v_{AB} \times v_{AC}

        where :math:`v_{AB}` is the vector from vertex A to vertex B.

        Returns
        -------
        Vector
            Normal vector.

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

        >>> Triangle([0, 0], [1, 0], [0, 1]).normal()
        Vector([0, 0, 1])

        The normal vector is not necessarily a unit vector.

        >>> Triangle([0, 0], [2, 0], [0, 2]).normal()
        Vector([0, 0, 4])

        The direction of the normal vector is dependent on the order of the vertices.

        >>> Triangle([0, 0], [0, 1], [1, 0]).normal()
        Vector([ 0,  0, -1])

        """
        vector_ab = Vector.from_points(self.point_a, self.point_b)
        vector_ac = Vector.from_points(self.point_a, self.point_c)

        return vector_ab.cross(vector_ac)
Exemplo n.º 16
0
    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 plus/minus.
        distances = -dot + pm * math.sqrt(discriminant)

        point_a, point_b = line.point + distances.reshape(-1, 1) * vector_unit

        return point_a, point_b
Exemplo n.º 17
0
    def project_point(self, point: array_like) -> Point:
        """
        Project a point onto the circle or sphere.

        Parameters
        ----------
        point : array_like
            Input point.

        Returns
        -------
        Point
            Point projected onto the circle or sphere.

        Raises
        ------
        ValueError
            If the input point is the center of the circle or sphere.

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

        >>> circle = Circle([0, 0], 1)

        >>> circle.project_point([1, 1]).round(3)
        Point([0.707, 0.707])

        >>> circle.project_point([-6, 3]).round(3)
        Point([-0.894,  0.447])

        >>> circle.project_point([0, 0])
        Traceback (most recent call last):
        ...
        ValueError: The point must not be the center of the circle or sphere.

        >>> from skspatial.objects import Sphere

        >>> Sphere([0, 0, 0], 2).project_point([1, 2, 3]).round(3)
        Point([0.535, 1.069, 1.604])

        """
        if self.point.is_equal(point):
            raise ValueError(
                "The point must not be the center of the circle or sphere.")

        vector_to_point = Vector.from_points(self.point, point)

        return self.point + self.radius * vector_to_point.unit()
Exemplo n.º 18
0
    def side_point(self, point: array_like) -> int:
        """
        Find the side of the line where a point lies.

        The line and point must be 2D.

        Parameters
        ----------
        point : array_like
            Input point.

        Returns
        -------
        int
            -1 if the point is left of the line.
            0 if the point is on the line.
            1 if the point is right of the line.

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

        >>> line = Line([0, 0], [1, 1])

        The point is on the line.

        >>> line.side_point([2, 2])
        0

        The point is to the right of the line.

        >>> line.side_point([5, 3])
        1

        The point is to the left of the line.

        >>> line.side_point([5, 10])
        -1

        """
        vector_to_point = Vector.from_points(self.point, point)

        return self.direction.side_vector(vector_to_point)
Exemplo n.º 19
0
    def distance_point_signed(self, point: array_like) -> np.float64:
        """
        Return the signed distance from a point to the plane.

        Parameters
        ----------
        point : array_like
            Input point.

        Returns
        -------
        np.float64
            Signed distance from the point to the plane.

        References
        ----------
        http://mathworld.wolfram.com/Point-PlaneDistance.html

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

        >>> plane = Plane([0, 0, 0], [0, 0, 1])

        >>> plane.distance_point_signed([5, 2, 0])
        0.0

        >>> plane.distance_point_signed([5, 2, 1])
        1.0

        >>> plane.distance_point([5, 2, -4])
        4.0
        >>> plane.distance_point_signed([5, 2, -4])
        -4.0

        """
        vector_to_point = Vector.from_points(self.point, point)

        return self.normal.scalar_projection(vector_to_point)
Exemplo n.º 20
0
    def distance_line(self, other: 'Line') -> np.float64:
        """
        Return the shortest distance from the line to another.

        Parameters
        ----------
        other : Line
            Other line.

        Returns
        -------
        np.float64
            Distance between the lines.

        References
        ----------
        http://mathworld.wolfram.com/Line-LineDistance.html

        Examples
        --------
        There are three cases:

        1. The lines intersect (i.e., they are coplanar and not parallel).

        >>> from skspatial.objects import Line
        >>> line_a = Line([1, 2], [4, 3])
        >>> line_b = Line([-4, 1], [7, 23])

        >>> line_a.distance_line(line_b)
        0.0

        2. The lines are parallel.

        >>> line_a = Line([0, 0], [1, 0])
        >>> line_b = Line([0, 5], [-1, 0])

        >>> line_a.distance_line(line_b)
        5.0

        3. The lines are skew.

        >>> line_a = Line([0, 0, 0], [1, 0, 1])
        >>> line_b = Line([1, 0, 0], [1, 1, 1])

        >>> line_a.distance_line(line_b).round(3)
        0.707

        """
        if self.direction.is_parallel(other.direction):
            # The lines are parallel.
            # The distance between the lines is the distance from line point B to line A.
            distance = self.distance_point(other.point)

        elif self.is_coplanar(other):
            # The lines must intersect, since they are coplanar and not parallel.
            distance = np.float64(0)

        else:
            # The lines are skew.
            vector_ab = Vector.from_points(self.point, other.point)
            vector_perpendicular = self.direction.cross(other.direction)

            distance = abs(vector_ab.dot(vector_perpendicular)) / vector_perpendicular.norm()

        return distance
Exemplo n.º 21
0
class Cylinder(_BaseSpatial, _ToPointsMixin):
    """
    A cylinder in space.

    The cylinder is defined by a point at its base, a vector along its axis, and a radius.

    Parameters
    ----------
    point : array_like
        Centre of the cylinder base.
    vector : array_like
        Normal vector of the cylinder base (the vector along the cylinder axis).
        The length of the cylinder is the length of this vector.
    radius : {int, float}
        Radius of the cylinder.
        This is the radius of the circular base.

    Attributes
    ----------
    point : Point
        Centre of the cylinder base.
    vector : Vector
        Normal vector of the cylinder base.
    radius : {int, float}
        Radius of the cylinder.
    dimension : int
        Dimension of the cylinder.

    Raises
    ------
    ValueError
        If the point or vector are not 3D.
        If the vector is all zeros.
        If the radius is zero.

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

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

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

    >>> Cylinder([0, 0, 0], [0, 0, 0], 1)
    Traceback (most recent call last):
    ...
    ValueError: The vector must not be the zero vector.

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

    >>> cylinder = Cylinder([0, 0, 0], [0, 0, 1], 1)

    >>> cylinder
    Cylinder(point=Point([0, 0, 0]), vector=Vector([0, 0, 1]), radius=1)

    >>> cylinder.point
    Point([0, 0, 0])
    >>> cylinder.vector
    Vector([0, 0, 1])
    >>> cylinder.radius
    1
    >>> cylinder.dimension
    3

    """

    def __init__(self, point: array_like, vector: array_like, radius: float):

        self.point = Point(point)
        self.vector = Vector(vector)

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

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

        if self.vector.is_zero():
            raise ValueError("The vector must not be the zero vector.")

        if not radius > 0:
            raise ValueError("The radius must be positive.")

        self.radius = radius

        self.dimension = self.point.dimension

    def __repr__(self) -> str:

        repr_point = np.array_repr(self.point)
        repr_vector = np.array_repr(self.vector)

        return f"Cylinder(point={repr_point}, vector={repr_vector}, radius={self.radius})"

    @classmethod
    def from_points(cls, point_a: array_like, point_b: array_like, radius: float) -> Cylinder:
        """
        Instantiate a cylinder from two points and a radius.

        Parameters
        ----------
        point_a, point_b : array_like
            The centres of the two circular ends.
        radius : float
            The cylinder radius.

        Returns
        -------
        Cylinder
            The cylinder defined by the two points and the radius.

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

        >>> Cylinder.from_points([0, 0, 0], [0, 0, 1], 1)
        Cylinder(point=Point([0, 0, 0]), vector=Vector([0, 0, 1]), radius=1)

        >>> Cylinder.from_points([0, 0, 0], [0, 0, 2], 1)
        Cylinder(point=Point([0, 0, 0]), vector=Vector([0, 0, 2]), radius=1)

        """
        vector_ab = Vector.from_points(point_a, point_b)

        return cls(point_a, vector_ab, radius)

    def length(self) -> np.float64:
        """
        Return the length of the cylinder.

        This is the length of the vector used to initialize the cylinder.

        Returns
        -------
        np.float64
            Length of the cylinder.

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

        >>> Cylinder([0, 0, 0], [0, 0, 1], 1).length()
        1.0

        >>> Cylinder([0, 0, 0], [0, 0, 2], 1).length()
        2.0

        >>> Cylinder([0, 0, 0], [1, 1, 1], 1).length().round(3)
        1.732

        """
        return self.vector.norm()

    def lateral_surface_area(self) -> np.float64:
        """
        Return the lateral surface area of the cylinder.

        Returns
        -------
        np.float64
            Lateral surface area of the cylinder.

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

        >>> Cylinder([0, 0, 0], [0, 0, 1], 1).lateral_surface_area().round(3)
        6.283

        >>> Cylinder([0, 0, 0], [0, 0, 1], 2).lateral_surface_area().round(3)
        12.566

        >>> Cylinder([0, 0, 0], [0, 0, 2], 2).lateral_surface_area().round(3)
        25.133

        """
        return 2 * np.pi * self.radius * self.length()

    def surface_area(self) -> np.float64:
        """
        Return the total surface area of the cylinder.

        This is the lateral surface area plus the area of the two circular caps.

        Returns
        -------
        np.float64
            Total surface area of the cylinder.

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

        >>> Cylinder([0, 0, 0], [0, 0, 1], 1).surface_area().round(3)
        12.566

        >>> Cylinder([0, 0, 0], [0, 0, 1], 2).surface_area().round(3)
        37.699

        >>> Cylinder([0, 0, 0], [0, 0, 2], 2).surface_area().round(3)
        50.265

        """
        return self.lateral_surface_area() + 2 * np.pi * self.radius**2

    def volume(self) -> np.float64:
        r"""
        Return the volume of the cylinder.

        The volume :math:`V` of a cylinder with radius :math:`r` and length :math:`l` is

        .. math:: V = \pi r^2 l

        Returns
        -------
        np.float64
            Volume of the cylinder.

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

        >>> Cylinder([0, 0, 0], [0, 0, 1], 1).volume().round(5)
        3.14159

        The length of the vector sets the length of the cylinder.

        >>> Cylinder([0, 0, 0], [0, 0, 2], 1).volume().round(5)
        6.28319

        """
        return np.pi * self.radius**2 * self.length()

    def is_point_within(self, point: array_like) -> bool:
        """
        Check if a point is within the cylinder.

        This also includes a point on the surface.

        Parameters
        ----------
        point : array_like
            Input point

        Returns
        -------
        bool
            True if the point is within the cylinder.

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

        >>> cylinder = Cylinder([0, 0, 0], [0, 0, 1], 1)

        >>> cylinder.is_point_within([0, 0, 0])
        True
        >>> cylinder.is_point_within([0, 0, 1])
        True
        >>> cylinder.is_point_within([0, 0, 2])
        False
        >>> cylinder.is_point_within([0, 0, -1])
        False
        >>> cylinder.is_point_within([1, 0, 0])
        True
        >>> cylinder.is_point_within([0, 1, 0])
        True
        >>> cylinder.is_point_within([1, 1, 0])
        False

        """
        line_axis = Line(self.point, self.vector)
        distance_to_axis = line_axis.distance_point(point)

        within_radius = distance_to_axis <= self.radius

        between_cap_planes = _between_cap_planes(self, point)

        return within_radius and between_cap_planes

    def intersect_line(
        self,
        line: Line,
        n_digits: Optional[int] = None,
        infinite: bool = True,
    ) -> Tuple[Point, Point]:
        """
        Intersect the cylinder with a 3D line.

        By default, this method treats the cylinder as infinite along its axis (i.e., without caps).

        Parameters
        ----------
        line : Line
            Input 3D line.
        n_digits : int, optional
            Additional keywords passed to :func:`round`.
            This is used to round the coefficients of the quadratic equation.
        infinite : bool
            If True, the cylinder is treated as infinite along its axis (i.e., without caps).

        Returns
        -------
        point_a, point_b: Point
            The two intersection points of the line with the cylinder, if they exist.

        Raises
        ------
        ValueError
            If the line is not 3D.
            If the line does not intersect the cylinder at one or two points.

        References
        ----------
        https://mrl.cs.nyu.edu/~dzorin/rendering/lectures/lecture3/lecture3.pdf

        Examples
        --------
        >>> from skspatial.objects import Line, Cylinder

        >>> cylinder = Cylinder([0, 0, 0], [0, 0, 1], 1)
        >>> line = Line([0, 0, 0], [1, 0, 0])


        Intersection with an infinite cylinder.

        >>> cylinder.intersect_line(line)
        (Point([-1.,  0.,  0.]), Point([1., 0., 0.]))

        >>> line = Line([1, 2, 3], [1, 2, 3])

        >>> point_a, point_b = cylinder.intersect_line(line)

        >>> point_a.round(3)
        Point([-0.447, -0.894, -1.342])
        >>> point_b.round(3)
        Point([0.447, 0.894, 1.342])

        >>> cylinder.intersect_line(Line([0, 0], [1, 2]))
        Traceback (most recent call last):
        ...
        ValueError: The line must be 3D.

        >>> cylinder.intersect_line(Line([0, 0, 2], [0, 0, 1]))
        Traceback (most recent call last):
        ...
        ValueError: The line does not intersect the cylinder.

        >>> cylinder.intersect_line(Line([2, 0, 0], [0, 1, 1]))
        Traceback (most recent call last):
        ...
        ValueError: The line does not intersect the cylinder.


        Intersection with a finite cylinder.

        >>> point_a, point_b = cylinder.intersect_line(Line([0, 0, 0], [0, 0, 1]), infinite=False)

        >>> point_a
        Point([0., 0., 0.])
        >>> point_b
        Point([0., 0., 1.])

        >>> cylinder = Cylinder([0, 0, 0], [0, 0, 5], 1)

        >>> point_a, point_b = cylinder.intersect_line(Line([0, 0, 0], [1, 0, 1]), infinite=False)

        >>> point_a
        Point([0., 0., 0.])
        >>> point_b
        Point([1., 0., 1.])

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

        if infinite:
            return _intersect_line_with_infinite_cylinder(self, line, n_digits)

        return _intersect_line_with_finite_cylinder(self, line, n_digits)

    def to_mesh(self, n_along_axis: int = 100, n_angles: int = 30) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
        """
        Return coordinate matrices for the 3D surface of the cylinder.

        Parameters
        ----------
        n_along_axis : int
            Number of intervals along the axis of the cylinder.
        n_angles : int
            Number of angles distributed around the circle.

        Returns
        -------
        X, Y, Z: (n_angles, n_angles) ndarray
            Coordinate matrices.

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

        >>> X, Y, Z = Cylinder([0, 0, 0], [0, 0, 1], 1).to_mesh(2, 4)

        >>> X.round(3)
        array([[-1. , -1. ],
               [ 0.5,  0.5],
               [ 0.5,  0.5],
               [-1. , -1. ]])

        >>> Y.round(3)
        array([[ 0.   ,  0.   ],
               [ 0.866,  0.866],
               [-0.866, -0.866],
               [-0.   , -0.   ]])

        >>> Z.round(3)
        array([[0., 1.],
               [0., 1.],
               [0., 1.],
               [0., 1.]])

        """
        # Unit vector along the cylinder axis.
        v_axis = self.vector.unit()

        # Arbitrary unit vector in a direction other than the axis.
        # This is used to get a vector perpendicular to the axis.
        v_different_direction = v_axis.different_direction()

        # Two unit vectors that are mutually perpendicular
        # and perpendicular to the cylinder axis.
        # These are used to define the points on the cylinder surface.
        u_1 = v_axis.cross(v_different_direction)
        u_2 = v_axis.cross(u_1)

        # The cylinder surface ranges over t from 0 to length of axis,
        # and over theta from 0 to 2 * pi.
        t = np.linspace(0, self.length(), n_along_axis)
        theta = np.linspace(0, 2 * np.pi, n_angles)

        # use meshgrid to make 2d arrays
        t, theta = np.meshgrid(t, theta)

        X, Y, Z = [
            self.point[i] + v_axis[i] * t + self.radius * np.sin(theta) * u_1[i] + self.radius * np.cos(theta) * u_2[i]
            for i in range(3)
        ]

        return X, Y, Z

    def plot_3d(self, ax_3d: Axes3D, n_along_axis: int = 100, n_angles: int = 30, **kwargs) -> None:
        """
        Plot a 3D cylinder.

        Parameters
        ----------
        ax_3d : Axes3D
            Instance of :class:`~mpl_toolkits.mplot3d.axes3d.Axes3D`.
        n_along_axis : int
            Number of intervals along the axis of the cylinder.
        n_angles : int
            Number of angles distributed around the circle.
        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 Cylinder

            >>> fig = plt.figure()
            >>> ax = fig.add_subplot(111, projection='3d')

            >>> cylinder = Cylinder([5, 3, 1], [1, 0, 1], 2)

            >>> cylinder.plot_3d(ax, alpha=0.2)
            >>> cylinder.point.plot_3d(ax, s=100)

        """
        X, Y, Z = self.to_mesh(n_along_axis, n_angles)

        ax_3d.plot_surface(X, Y, Z, **kwargs)
Exemplo n.º 22
0
    def intersect_line(self, other: Line, **kwargs) -> Point:
        """
        Intersect the line with another.

        The lines must be coplanar and not parallel.

        Parameters
        ----------
        other : Line
            Other line.
        kwargs : dict, optional
            Additional keywords passed to :meth:`Vector.is_parallel`.

        Returns
        -------
        Point
            The point at the intersection.

        Raises
        ------
        ValueError
            If the lines don't have the same dimension.
            If the line dimension is greater than three.
            If the lines are parallel.
            If the lines are not coplanar.

        References
        ----------
        http://mathworld.wolfram.com/Line-LineIntersection.html

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

        >>> line_a = Line([0, 0], [1, 0])
        >>> line_b = Line([5, 5], [0, 1])
        >>> line_a.intersect_line(line_b)
        Point([5., 0.])

        >>> line_a = Line([0, 0, 0], [1, 1, 1])
        >>> line_b = Line([5, 5, 0], [0, 0, -8])
        >>> line_a.intersect_line(line_b)
        Point([5., 5., 5.])

        >>> line_a = Line([0, 0, 0], [1, 0, 0])
        >>> line_b = Line([0, 0], [1, 1])
        >>> line_a.intersect_line(line_b)
        Traceback (most recent call last):
        ...
        ValueError: The lines must have the same dimension.

        >>> line_a = Line(4 * [0], [1, 0, 0, 0])
        >>> line_b = Line(4 * [0], [0, 0, 0, 1])
        >>> line_a.intersect_line(line_b)
        Traceback (most recent call last):
        ...
        ValueError: The line dimension cannot be greater than 3.

        >>> line_a = Line([0, 0], [0, 1])
        >>> line_b = Line([0, 1], [0, 1])

        >>> line_a = Line([0, 0], [1, 0])
        >>> line_b = Line([0, 1], [2, 0])
        >>> line_a.intersect_line(line_b)
        Traceback (most recent call last):
        ...
        ValueError: The lines must not be parallel.

        >>> line_a = Line([1, 2, 3], [-4, 1, 1])
        >>> line_b = Line([4, 5, 6], [3, 1, 5])
        >>> line_a.intersect_line(line_b)
        Traceback (most recent call last):
        ...
        ValueError: The lines must be coplanar.

        """
        if self.dimension != other.dimension:
            raise ValueError("The lines must have the same dimension.")

        if self.dimension > 3 or other.dimension > 3:
            raise ValueError("The line dimension cannot be greater than 3.")

        if self.direction.is_parallel(other.direction, **kwargs):
            raise ValueError("The lines must not be parallel.")

        if not self.is_coplanar(other):
            raise ValueError("The lines must be coplanar.")

        # Vector from line A to line B.
        vector_ab = Vector.from_points(self.point, other.point)

        # Vector perpendicular to both lines.
        vector_perpendicular = self.direction.cross(other.direction)

        num = vector_ab.cross(other.direction).dot(vector_perpendicular)
        denom = vector_perpendicular.norm()**2

        # Vector along line A to the intersection point.
        vector_a_scaled = num / denom * self.direction

        return self.point + vector_a_scaled
Exemplo n.º 23
0
    def best_fit(cls, points: array_like, **kwargs) -> 'Plane':
        """
        Return the plane of best fit for a set of 3D points.

        Parameters
        ----------
        points : array_like
             Input 3D points.
        kwargs : dict, optional
            Additional keywords passed to :func:`numpy.linalg.svd`

        Returns
        -------
        Plane
            The plane of best fit.

        Raises
        ------
        ValueError
            If the points are collinear or are not 3D.

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

        >>> points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]
        >>> plane = Plane.best_fit(points)

        The point on the plane is the centroid of the points.

        >>> plane.point
        Point([0.25, 0.25, 0.25])

        The plane normal is a unit vector.

        >>> plane.normal.round(3)
        Vector([-0.577, -0.577, -0.577])

        >>> points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]]

        >>> Plane.best_fit(points)
        Plane(point=Point([0.5, 0.5, 0. ]), normal=Vector([0., 0., 1.]))

        >>> Plane.best_fit(points, full_matrices=False)
        Plane(point=Point([0.5, 0.5, 0. ]), normal=Vector([0., 0., 1.]))

        """
        points = Points(points)

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

        if points.are_collinear(tol=0):
            raise ValueError("The points must not be collinear.")

        points_centered, centroid = points.mean_center(return_centroid=True)

        u, sigma, _ = np.linalg.svd(points_centered.T, **kwargs)
        error = min(sigma)
        normal = Vector(u[:, -1])

        return cls(centroid, normal, error=error)
Exemplo n.º 24
0
    def best_fit(cls,
                 points: array_like,
                 tol: Optional[float] = None,
                 **kwargs) -> Plane:
        """
        Return the plane of best fit for a set of 3D points.

        Parameters
        ----------
        points : array_like
             Input 3D points.
        tol : float | None, optional
            Keyword passed to :meth:`Points.are_collinear` (default None).
        kwargs : dict, optional
            Additional keywords passed to :func:`numpy.linalg.svd`

        Returns
        -------
        Plane
            The plane of best fit.

        Raises
        ------
        ValueError
            If the points are collinear or are not 3D.

        References
        ----------
        Using SVD for some fitting problems
        Inge Söderkvist
        Algorithm 3.1
        https://www.ltu.se/cms_fs/1.51590!/svd-fitting.pdf

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

        >>> points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]
        >>> plane = Plane.best_fit(points)

        The point on the plane is the centroid of the points.

        >>> plane.point
        Point([0.25, 0.25, 0.25])

        The plane normal is a unit vector.

        >>> plane.normal.round(3)
        Vector([-0.577, -0.577, -0.577])

        >>> points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]]

        >>> Plane.best_fit(points)
        Plane(point=Point([0.5, 0.5, 0. ]), normal=Vector([0., 0., 1.]))

        >>> Plane.best_fit(points, full_matrices=False)
        Plane(point=Point([0.5, 0.5, 0. ]), normal=Vector([0., 0., 1.]))

        """
        points = Points(points)

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

        if points.are_collinear(tol=tol):
            raise ValueError("The points must not be collinear.")

        points_centered, centroid = points.mean_center(return_centroid=True)

        u, _, _ = np.linalg.svd(points_centered.T, **kwargs)
        normal = Vector(u[:, 2])

        return cls(centroid, normal)
Exemplo n.º 25
0
class _BaseLinePlane(_BaseSpatial):
    """Private parent class for Line and Plane."""
    def __init__(self, point: array_like, vector: array_like, **kwargs):

        self.point = Point(point)
        self.vector = Vector(vector)

        if self.point.dimension != self.vector.dimension:
            raise ValueError(
                "The point and vector must have the same dimension.")

        if self.vector.is_zero(**kwargs):
            raise ValueError("The vector must not be the zero vector.")

        self.dimension = self.point.dimension

    def __repr__(self) -> str:

        name_class = type(self).__name__
        name_vector = inspect.getfullargspec(type(self)).args[-1]

        repr_point = np.array_repr(self.point)
        repr_vector = np.array_repr(self.vector)

        return f"{name_class}(point={repr_point}, {name_vector}={repr_vector})"

    def contains_point(self, point: array_like, **kwargs: float) -> bool:
        """Check if the line/plane contains a point."""
        return _contains_point(self, point, **kwargs)

    def is_close(self, other: array_like, **kwargs: float) -> bool:
        """
        Check if the line/plane is almost equivalent to another line/plane.

        The points must be close and the vectors must be parallel.

        Parameters
        ----------
        other : object
             Line or Plane.
        kwargs : dict, optional
            Additional keywords passed to :func:`math.isclose`.

        Returns
        -------
        bool
            True if the objects are almost equivalent; false otherwise.

        Raises
        ------
        TypeError
            If the input doesn't have the same type as the object.

        Examples
        --------
        >>> from skspatial.objects import Line, Plane

        >>> line_a = Line(point=[0, 0], direction=[1, 0])
        >>> line_b = Line(point=[0, 0], direction=[-2, 0])
        >>> line_a.is_close(line_b)
        True

        >>> line_b = Line(point=[50, 0], direction=[-4, 0])
        >>> line_a.is_close(line_b)
        True

        >>> line_b = Line(point=[50, 29], direction=[-4, 0])
        >>> line_a.is_close(line_b)
        False

        >>> plane_a = Plane(point=[0, 0, 0], normal=[0, 0, 5])
        >>> plane_b = Plane(point=[23, 45, 0], normal=[0, 0, -20])
        >>> plane_a.is_close(plane_b)
        True

        >>> line_a.is_close(plane_a)
        Traceback (most recent call last):
        ...
        TypeError: The input must have the same type as the object.

        """
        if not isinstance(other, type(self)):
            raise TypeError("The input must have the same type as the object.")

        contains_point = self.contains_point(other.point, **kwargs)
        is_parallel = self.vector.is_parallel(other.vector, **kwargs)

        return contains_point and is_parallel

    def sum_squares(self, points: array_like) -> np.float64:

        return _sum_squares(self, points)
Exemplo n.º 26
0
    def intersect_line(self, other: 'Line') -> Point:
        """
        Intersect the line with another.

        The lines must be coplanar and not parallel.

        Parameters
        ----------
        other : Line
            Other line.

        Returns
        -------
        Point
            The point at the intersection.

        Raises
        ------
        ValueError
            If the lines are parallel or are not coplanar.

        References
        ----------
        http://mathworld.wolfram.com/Line-LineIntersection.html

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

        >>> line_a = Line([0, 0], [1, 0])
        >>> line_b = Line([5, 5], [0, 1])

        >>> line_a.intersect_line(line_b)
        Point([5., 0.])

        >>> line_b = Line([0, 1], [2, 0])
        >>> line_a.intersect_line(line_b)
        Traceback (most recent call last):
        ...
        ValueError: The lines must not be parallel.

        >>> line_a = Line([1, 2, 3], [-4, 1, 1])
        >>> line_b = Line([4, 5, 6], [3, 1, 5])

        >>> line_a.intersect_line(line_b)
        Traceback (most recent call last):
        ...
        ValueError: The lines must be coplanar.

        >>> line_a = Line([0, 0, 0], [1, 1, 1])
        >>> line_b = Line([5, 5, 0], [0, 0, -8])

        >>> line_a.intersect_line(line_b)
        Point([5., 5., 5.])

        """
        if self.direction.is_parallel(other.direction, rel_tol=0, abs_tol=0):
            raise ValueError("The lines must not be parallel.")

        if not self.is_coplanar(other):
            raise ValueError("The lines must be coplanar.")

        # Vector from line A to line B.
        vector_ab = Vector.from_points(self.point, other.point)

        # Vector perpendicular to both lines.
        vector_perpendicular = self.direction.cross(other.direction)

        num = vector_ab.cross(other.direction).dot(vector_perpendicular)
        denom = vector_perpendicular.norm() ** 2

        # Vector along line A to the intersection point.
        vector_a_scaled = num / denom * self.direction

        return self.point + vector_a_scaled