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
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 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)
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
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
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)
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()
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
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
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)
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 __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 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)
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)
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)
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
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()
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)
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)
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
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)
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
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)
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)
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)
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