"""Module for the Points class."""
from __future__ import annotations
from typing import cast
import numpy as np
from matplotlib.axes import Axes
from mpl_toolkits.mplot3d import Axes3D
from numpy.linalg import matrix_rank
from skspatial.objects._base_array import _BaseArray2D
from skspatial.objects.point import Point
from skspatial.plotting import _scatter_2d, _scatter_3d
class Points(_BaseArray2D):
"""
Multiple points in space implemented as a 2D array.
The array is a subclass of :class:`numpy.ndarray`.
Each row in the array represents a point in space.
Parameters
----------
points : array_like
(N, D) array representing N points with dimension D.
Raises
------
ValueError
If the array is empty, the values are not finite,
or the dimension is not two.
Examples
--------
>>> from skspatial.objects import Points
>>> points = Points([[1, 2, 0], [5, 4, 3]])
>>> points
Points([[1, 2, 0],
[5, 4, 3]])
>>> points.dimension
3
The object inherits methods from :class:`numpy.ndarray`.
>>> points.mean(axis=0)
array([3. , 3. , 1.5])
>>> Points([[]])
Traceback (most recent call last):
...
ValueError: The array must not be empty.
>>> import numpy as np
>>> Points([[1, 2], [1, np.nan]])
Traceback (most recent call last):
...
ValueError: The values must all be finite.
>>> Points([1, 2, 3])
Traceback (most recent call last):
...
ValueError: The array must be 2D.
"""
[docs] def unique(self) -> Points:
"""
Return unique points.
The output contains the unique rows of the original array.
Returns
-------
Points
(N, D) array of N unique points with dimension D.
Examples
--------
>>> from skspatial.objects import Points
>>> points = Points([[1, 2, 3], [2, 3, 4], [1, 2, 3]])
>>> points.unique()
Points([[1, 2, 3],
[2, 3, 4]])
"""
return Points(np.unique(self, axis=0))
[docs] def centroid(self) -> Point:
"""
Return the centroid of the points.
Returns
-------
Point
Centroid of the points.
Examples
--------
>>> from skspatial.objects import Points
>>> Points([[1, 2, 3], [2, 2, 3]]).centroid()
Point([1.5, 2. , 3. ])
"""
centroid_ = cast(np.ndarray, self.mean(axis=0))
return Point(centroid_)
[docs] def mean_center(self, return_centroid: bool = False):
"""
Mean-center the points by subtracting the centroid.
Parameters
----------
return_centroid : bool, optional
If True, also return the original centroid of the points.
Returns
-------
points_centered : (N, D) Points
Array of N mean-centered points with dimension D.
centroid : (D,) Point, optional
Original centroid of the points. Only provided if `return_centroid` is True.
Examples
--------
>>> from skspatial.objects import Points
>>> points_centered, centroid = Points([[4, 4, 4], [2, 2, 2]]).mean_center(return_centroid=True)
>>> points_centered
Points([[ 1., 1., 1.],
[-1., -1., -1.]])
>>> centroid
Point([3., 3., 3.])
The centroid of the centered points is the origin.
>>> points_centered.centroid()
Point([0., 0., 0.])
"""
centroid = self.centroid()
points_centered = self - centroid
if return_centroid:
return points_centered, centroid
return points_centered
def normalize_distance(self) -> Points:
"""
Normalize the distances of the points from the origin.
The normalized points lie within a unit sphere centered on the origin.
Returns
-------
Points
Normalized points.
Examples
--------
>>> from skspatial.objects import Points
>>> points = Points([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> points.normalize_distance().round(3)
Points([[0.072, 0.144, 0.215],
[0.287, 0.359, 0.431],
[0.503, 0.574, 0.646]])
The transformation can be chained with mean centering.
>>> points.mean_center().normalize_distance().round(3)
Points([[-0.577, -0.577, -0.577],
[ 0. , 0. , 0. ],
[ 0.577, 0.577, 0.577]])
"""
distances_to_points = np.linalg.norm(self, axis=1)
return self / distances_to_points.max()
[docs] def affine_rank(self, **kwargs) -> np.int64:
"""
Return the affine rank of the points.
The affine rank is the dimension of the smallest affine space that contains the points.
A rank of 1 means the points are collinear, and a rank of 2 means they are coplanar.
Parameters
----------
kwargs : dict, optional
Additional keywords passed to :func:`numpy.linalg.matrix_rank`
Returns
-------
np.int64
Affine rank of the points.
Examples
--------
>>> from skspatial.objects import Points
>>> Points([[5, 5], [5, 5]]).affine_rank()
np.int64(0)
>>> Points([[5, 3], [-6, 20]]).affine_rank()
np.int64(1)
>>> Points([[0, 0], [1, 1], [2, 2]]).affine_rank()
np.int64(1)
>>> Points([[0, 0], [1, 0], [2, 2]]).affine_rank()
np.int64(2)
>>> Points([[0, 1, 0], [1, 1, 0], [2, 2, 2]]).affine_rank()
np.int64(2)
>>> Points([[0, 0], [0, 1], [1, 0], [1, 1]]).affine_rank()
np.int64(2)
>>> Points([[1, 3, 2], [3, 4, 5], [2, 1, 5], [5, 9, 8]]).affine_rank()
np.int64(3)
"""
# Remove duplicate points so they do not affect the centroid.
points_centered = self.unique().mean_center()
return matrix_rank(points_centered, **kwargs)
[docs] def are_concurrent(self, **kwargs) -> bool:
"""
Check if the points are all contained in one point.
Parameters
----------
kwargs : dict, optional
Additional keywords passed to :func:`numpy.linalg.matrix_rank`
Returns
-------
bool
True if points are concurrent; false otherwise.
Examples
--------
>>> from skspatial.objects import Points
>>> Points([[0, 0], [1, 1], [1, 1]]).are_concurrent()
False
>>> Points([[1, 1], [1, 1], [1, 1]]).are_concurrent()
True
"""
return bool(self.affine_rank(**kwargs) == 0)
[docs] def are_collinear(self, **kwargs) -> bool:
"""
Check if the points are all contained in one line.
Parameters
----------
kwargs : dict, optional
Additional keywords passed to :func:`numpy.linalg.matrix_rank`
Returns
-------
bool
True if points are collinear; false otherwise.
Examples
--------
>>> from skspatial.objects import Points
>>> Points(([0, 0, 0], [1, 2, 3], [2, 4, 6])).are_collinear()
True
>>> Points(([0, 0, 0], [1, 2, 3], [5, 2, 0])).are_collinear()
False
>>> Points(([0, 0], [1, 2], [5, 2], [6, 3])).are_collinear()
False
"""
return bool(self.affine_rank(**kwargs) <= 1)
[docs] def are_coplanar(self, **kwargs) -> bool:
"""
Check if the points are all contained in one plane.
Parameters
----------
kwargs : dict, optional
Additional keywords passed to :func:`numpy.linalg.matrix_rank`
Returns
-------
bool
True if points are coplanar; false otherwise.
Examples
--------
>>> from skspatial.objects import Points
>>> Points([[1, 2], [9, -18], [12, 4], [2, 1]]).are_coplanar()
True
>>> Points([[1, 2], [9, -18], [12, 4], [2, 2]]).are_coplanar()
True
>>> Points([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]).are_coplanar()
False
"""
return bool(self.affine_rank(**kwargs) <= 2)
[docs] def plot_2d(self, ax_2d: Axes, **kwargs) -> None:
"""
Plot the points on a 2D scatter plot.
Parameters
----------
ax_2d : Axes
Instance of :class:`~matplotlib.axes.Axes`.
kwargs : dict, optional
Additional keywords passed to :meth:`~matplotlib.axes.Axes.scatter`.
Examples
--------
.. plot::
:include-source:
>>> import matplotlib.pyplot as plt
>>> from skspatial.objects import Points
>>> fig, ax = plt.subplots()
>>> points = Points([[1, 2], [3, 4], [-4, 2], [-2, 3]])
>>> points.plot_2d(ax, c='k')
"""
_scatter_2d(ax_2d, self, **kwargs)
[docs] def plot_3d(self, ax_3d: Axes3D, **kwargs) -> None:
"""
Plot the points on a 3D scatter plot.
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.scatter`.
Examples
--------
.. plot::
:include-source:
>>> import matplotlib.pyplot as plt
>>> from mpl_toolkits.mplot3d import Axes3D
>>> from skspatial.objects import Points
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111, projection='3d')
>>> points = Points([[1, 2, 1], [3, 2, -7], [-4, 2, 2], [-2, 3, 1]])
>>> points.plot_3d(ax, s=75, depthshade=False)
"""
_scatter_3d(ax_3d, self, **kwargs)