PnsPy documentation#

Module reference#

Principal nested spheres (PNS) analysis [1].

[1]

Jung, Sungkyu, Ian L. Dryden, and James Stephen Marron. “Analysis of principal nested spheres.” Biometrika 99.3 (2012): 551-568.

pns.pns(x, n_components, tol=0.001, maxiter=None, lm_kwargs=None)[source]#

Principal nested spheres analysis.

Parameters:
xreal array of shape (N, d+1)

Data on a d-sphere.

n_componentsint

Target dimension.

tolfloat, default=1e-3

Convergence tolerance in radians.

maxiterint, optional

Maximum number of iterations for the optimization. If None, the number of iterations is not checked.

lm_kwargsdict, optional

Additional keyword arguments to be passed for Levenberg-Marquardt optimization. Passed to pns.pss().

Returns:
vslist of array

Principal axes.

rs1-D array of length (d+1-n_components)

Principal geodesic distances.

xis2-D array of shape (N, d+1-n_components)

Unscaled residuals.

x_transformreal array of shape (N, n_components)

Data transformed onto low-dimensional sphere.

Notes

The input data is \(x \in S^d \subset \mathbb{R}^{d+1}\).

The \(k\)-th element of vs, rs and xis are:

  1. The principal axis \(\hat{v}_{k} \in S^{d-k+1} \subset \mathbb{R}^{d-k+2}\),

  2. The principal geodesic distance \(\hat{r}_k \in \mathbb{R}\), and

  3. Unscaled residual \(\xi_{d-k}\).

Examples

>>> from pns import pns
>>> from pns.util import unit_sphere, circular_data, circle_3d
>>> x = circular_data([0, -1, 0])
>>> vs, rs, _, x_transform = pns(x, 2)
>>> import matplotlib.pyplot as plt
... fig = plt.figure()
... ax1 = fig.add_subplot(121, projection='3d', computed_zorder=False)
... ax1.plot_surface(*unit_sphere(), color='skyblue', alpha=0.6, edgecolor='gray')
... ax1.scatter(*x.T)
... ax1.scatter(*vs[0])
... ax1.plot(*circle_3d(vs[0], rs[0]), color="tab:orange", zorder=10)
... ax2 = fig.add_subplot(122)
... ax2.scatter(*x_transform.T)
... ax2.set_aspect("equal")
_images/index-1.png
pns.fit_transform(X, n_components, type='intrinsic', tol=0.001, maxiter=None, lm_kwargs=None)[source]#

Fit PNS and transform data into low-dimensional hypersphere.

Parameters:
Xreal array of shape (N, d+1)

Extrinsic coordinates of data on a d-dimensional hypersphere, embedded in a d+1-dimensional space.

n_componentsint

Target dimension.

type{‘intrinsic’, ‘extrinsic’}

Type of the output coordinates.

tolfloat, default=1e-3

Convergence tolerance in radians.

maxiterint, optional

Maximum number of iterations for the optimization. If None, the number of iterations is not checked.

lm_kwargsdict, optional

Additional keyword arguments to be passed for Levenberg-Marquardt optimization. Passed to pns.pns().

Returns:
vslist of array

Principal axes.

rs1-D array of length (d+1-n_components)

Principal geodesic distances.

X_transformarray of shape (N, n_components)

Coordinates of transformed data on a low-dimensional unit hypersphere.

Examples

Extrinsic transformation:

>>> from pns import fit_transform
>>> from pns.util import circular_data
>>> x = circular_data([0, -1, 0])
>>> _, _, x_transformed = fit_transform(x, n_components=2, type='extrinsic')
>>> import matplotlib.pyplot as plt
... plt.scatter(*x_transformed.T)
... plt.gca().set_aspect("equal")

Intrinsic transformation:

>>> import numpy as np
>>> from pns.util import unit_sphere
>>> _, _, x_transformed = fit_transform(x, n_components=2, type='intrinsic')
>>> fig = plt.figure()
... ax1 = fig.add_subplot(121, projection='3d', computed_zorder=False)
... ax1.plot_surface(*unit_sphere(), color='skyblue', edgecolor='gray')
... ax1.scatter(*x.T, c=x_transformed[:, 0])
... ax2 = fig.add_subplot(122)
... ax2.scatter(*x_transformed.T, c=x_transformed[:, 0])
... ax2.set_xlim(-np.pi, np.pi)
... ax2.set_ylim(-np.pi/2, np.pi/2)
_images/index-2_00.png
_images/index-2_01.png
pns.transform(X, vs, rs, n_components, type='intrinsic')[source]#

Transform data using the fitted PNS.

Parameters:
X(N, d+1) real array

Extrinsic coordinates of data on a d-dimensional hypersphere, embedded in a d+1-dimensional space.

vslist of k real arrays

Subsphere axes.

rslist of k scalars

Subsphere geodesic distances.

n_componentsint

Target dimension.

type{‘intrinsic’, ‘extrinsic’}

Type of the output coordinates.

Returns:
(N, n_components) real array

Coordinates of transformed data on a low-dimensional unit hypersphere.

Notes

If type is intrinsic, k must be d. If type is extrinsic, k must be at least d + 1 - n_components.

Examples

Extrinsic transformation:

>>> from pns import fit_transform, transform
>>> from pns.util import circular_data
>>> x = circular_data([0, -1, 0])
>>> vs, rs, _ = fit_transform(x, n_components=2, type='extrinsic')
>>> x_transformed = transform(x, vs, rs, n_components=2, type='extrinsic')
>>> import matplotlib.pyplot as plt
... plt.scatter(*x_transformed.T)
... plt.gca().set_aspect("equal")

Intrinsic transformation:

>>> import numpy as np
>>> from pns.util import unit_sphere
>>> vs, rs, _ = fit_transform(x, n_components=2, type='intrinsic')
>>> x_transformed = transform(x, vs, rs, n_components=2, type='intrinsic')
>>> fig = plt.figure()
... ax1 = fig.add_subplot(121, projection='3d', computed_zorder=False)
... ax1.plot_surface(*unit_sphere(), color='skyblue', edgecolor='gray')
... ax1.scatter(*x.T, c=x_transformed[:, 0])
... ax2 = fig.add_subplot(122)
... ax2.scatter(*x_transformed.T, c=x_transformed[:, 0])
... ax2.set_xlim(-np.pi, np.pi)
... ax2.set_ylim(-np.pi/2, np.pi/2)
_images/index-3_00.png
_images/index-3_01.png
pns.inverse_transform(X, vs, rs, type='intrinsic')[source]#

Inverse-transform data using the fitted PNS.

Parameters:
X(N, n_components) real array

Coordinates of transformed data on a low-dimensional unit hypersphere.

vslist of k real arrays

Subsphere axes.

rslist of k scalars

Subsphere geodesic distances.

type{‘intrinsic’, ‘extrinsic’}

Type of the output coordinates.

Returns:
(N, d+1) real array

Extrinsic coordinates of data on a d-dimensional hypersphere, embedded in a d+1-dimensional space.

Notes

If type is intrinsic, k must be d. If type is extrinsic, k must be at least d + 1 - n_components.

Examples

Extrinsic transformation:

>>> from pns import fit_transform, inverse_transform
>>> from pns.util import circular_data, unit_sphere
>>> x = circular_data([0, -1, 0])
>>> vs, rs, x_transformed = fit_transform(x, n_components=2, type='extrinsic')
>>> x_reconstructed = inverse_transform(x_transformed, vs, rs, type='extrinsic')
>>> import matplotlib.pyplot as plt
... fig = plt.figure()
... ax = fig.add_subplot(projection='3d', computed_zorder=False)
... ax.plot_surface(*unit_sphere(), color='skyblue', alpha=0.6, edgecolor='gray')
... ax.scatter(*x.T, marker=".", zorder=10)
... ax.scatter(*x_reconstructed.T, marker="x", zorder=10)

Intrinsic transformation:

>>> vs, rs, x_transformed = fit_transform(x, n_components=2, type='intrinsic')
>>> x_reconstructed = inverse_transform(x_transformed, vs, rs, type='intrinsic')
>>> import matplotlib.pyplot as plt
... ax = plt.figure().add_subplot(projection='3d', computed_zorder=False)
... ax.plot_surface(*unit_sphere(), color='skyblue', edgecolor='gray')
... ax.scatter(*x.T, marker="o", zorder=10)
... ax.scatter(*x_reconstructed.T, marker="x", zorder=10)
_images/index-4_00.png
_images/index-4_01.png

Data transformation#

Classes to transform data on a hypersphere.

pns.transformers.project(x, v, r)[source]#

Minimum-geodesic projection of points to a subsphere.

Parameters:
x(N, m+1) real array

Extrinsic coordinates of data on a d-dimensional hypersphere, embedded in a d+1-dimensional space.

v(m+1,) real array

Subsphere axis.

rscalar

Subsphere geodesic distance.

Returns:
xP(N, m+1) real array

Extrinsic coordinates of data on a d-dimensional hypersphere, projected onto the found principal subsphere.

res(N, 1) real array

Projection residuals.

Notes

This is the function \(P: S^{d-k+1} \to A_{d-k}(v_k, r_k ) \subset S^{d-k+1}\) for \(k = 1, 2, \ldots, d\) in the original paper. Here, \(A_{d-k}(v_k, r_k)\) is a subsphere of the hypersphere \(S^{d-k+1}\). The input and output data dimension are \(m+1\), where \(m = d-k+1\).

The resulting points have same number of components but their rank is reduced by one in the manifold.

Examples

>>> from pns.pss import pss
>>> from pns.transformers import project
>>> from pns.util import unit_sphere, circular_data
>>> x = circular_data([0, -1, 0]).reshape(-1, 3)
>>> v, r = pss(x)
>>> A, _ = project(x, v, r)
>>> import matplotlib.pyplot as plt
... ax = plt.figure().add_subplot(projection='3d', computed_zorder=False)
... ax.plot_surface(*unit_sphere(), color='skyblue', alpha=0.6, edgecolor='gray')
... ax.scatter(*x.T, marker="x")
... ax.scatter(*A.T, marker=".")
_images/index-5.png
pns.transformers.inverse_project(xP, res, v, r)[source]#

Inverse of project().

Parameters:
xP(N, m+1) real array

Extrinsic coordinates of data on a d-dimensional hypersphere, projected onto the found principal subsphere.

res(N, 1) real array

Projection residuals.

v(m+1,) real array

Subsphere axis.

rscalar

Subsphere geodesic distance.

Returns:
x(N, m+1) real array

Extrinsic coordinates of data on a d-dimensional hypersphere, embedded in a d+1-dimensional space.

Examples

>>> from pns.pss import pss
>>> from pns.transformers import project, inverse_project
>>> from pns.util import unit_sphere, circular_data
>>> x = circular_data([0, -1, 0])
>>> v, r = pss(x)
>>> xP, res = project(x, v, r)
>>> x_invprj = inverse_project(xP, res, v, r)
>>> import matplotlib.pyplot as plt
... ax = plt.figure().add_subplot(projection='3d', computed_zorder=False)
... ax.plot_surface(*unit_sphere(), color='skyblue', alpha=0.6, edgecolor='gray')
... ax.scatter(*xP.T, marker="x", zorder=10)
... ax.scatter(*x_invprj.T, marker=".")
_images/index-6.png
pns.transformers.embed(x, v, r)[source]#

Embed data on a sub-hypersphere to a low-dimensional unit hypersphere.

Parameters:
x(N, m+1) real array

Data \(x \in A_{m-1} \subset S^m \subset \mathbb{R}^{m+1}\), on a subsphere \(A_{m-1}\) of a unit hypersphere \(S^m\).

v(m+1,) real array

Sub-hypersphere axis.

rscalar

Sub-hypersphere geodesic distance.

Returns:
(N, m) real array

Data \(x^\dagger\) on a low-dimensional unit hypersphere \(S^{m-1}\).

Notes

This is the function \(f_k: A_{d-k}(v_k, r_k) \subset S^{d-k+1} \to S^{d-k}\) for \(k = 1, 2, \ldots, d-1\) in the original paper. Here, \(A_{d-k}(v_k, r_k)\) is a subsphere of the hypersphere \(S^{d-k+1}\). The input is \(x \in S^m \subset \mathbb{R}^{m+1}\) and the output is \(x^\dagger \in S^{m-1} \subset \mathbb{R}^{m}\), where \(m = d-k+1\).

Examples

>>> from pns.pss import pss
>>> from pns.transformers import embed
>>> from pns.util import unit_sphere, circular_data
>>> x = circular_data([0, -1, 0])
>>> v, r = pss(x)
>>> x_embed = embed(x, v, r)
>>> import matplotlib.pyplot as plt
... fig = plt.figure()
... ax1 = fig.add_subplot(121, projection='3d', computed_zorder=False)
... ax1.plot_surface(*unit_sphere(), color='skyblue', alpha=0.6, edgecolor='gray')
... ax1.scatter(*x.T, marker=".", zorder=10)
... ax2 = fig.add_subplot(122)
... ax2.scatter(*x_embed.T, marker=".", zorder=10)
... ax2.set_aspect("equal")
_images/index-7.png
pns.transformers.reconstruct(x, v, r)[source]#

Reconstruct data on a low-dimensional unit hypersphere. to a sub-hypersphere.

Parameters:
x(N, m) real array

Data \(x^\dagger\) on a low-dimensional unit hypersphere \(S^{m-1}\).

v(m+1,) real array

Sub-hypersphere axis.

rscalar

Sub-hypersphere geodesic distance.

Returns:
(N, m+1) real array

Data \(x \in A_{m-1} \subset S^m \subset \mathbb{R}^{m+1}\), on a subsphere \(A_{m-1}\) of a unit hypersphere \(S^m\).

Notes

This is the function \(f^{-1}_k: S^{d-k} \to A_{d-k}(v_k, r_k) \subset S^{d-k+1}\) for \(k = 1, 2, \ldots, d-1\) in the original paper. Here, \(A_{d-k}(v_k, r_k)\) is a subsphere of the hypersphere \(S^{d-k+1}\). The input is \(x^\dagger \in S^{m-1} \subset \mathbb{R}^{m}\) and the output is \(x \in S^m \subset \mathbb{R}^{m+1}\), where \(m = d-k+1\).

Examples

>>> import numpy as np
>>> from pns.transformers import reconstruct
>>> from pns.util import unit_sphere
>>> t = np.linspace(0, 2 * np.pi, 100)
>>> x = np.vstack((np.cos(t), np.sin(t))).T
>>> x_rec = reconstruct(x, np.array([0, -1, 0]), 0.5)
>>> import matplotlib.pyplot as plt
... fig = plt.figure()
... ax1 = fig.add_subplot(121)
... ax1.scatter(*x.T)
... ax1.set_aspect("equal")
... ax2 = fig.add_subplot(122, projection='3d', computed_zorder=False)
... ax2.plot_surface(*unit_sphere(), color='skyblue', alpha=0.6, edgecolor='gray')
... ax2.scatter(*x_rec.T)
_images/index-8.png

PSS detection#

Detect the principal subsphere by optimization.

pns.pss.pss(x, tol=0.001, maxiter=None, lm_kwargs=None)[source]#

Find the principal subsphere (PSS) from data on a hypersphere.

Parameters:
x(N, d+1) real array

Extrinsic coordinates of data on a d-dimensional hypersphere, embedded in a d+1-dimensional space.

tolfloat, default=1e-3

Convergence tolerance in radian.

maxiterint, optional

Maximum number of iterations for the optimization. If None, the number of iterations is not checked.

lm_kwargsdict, optional

Additional keyword arguments to be passed for Levenberg-Marquardt optimization. Follows the signature of scipy.optimize.least_squares().

Returns:
v(d+1,) real array

Estimated principal axis of the subsphere in extrinsic coordinates.

rscalar in [0, pi]

Geodesic distance from the pole by v to the estimated principal subsphere.

Notes

This function determines the best fitting subsphere \(\hat{A}_{d-k} = A_{d-k}(\hat{v}_k, \hat{r}_k) \subset S^{d-k+1}\) for \(k = 1, 2, \ldots, d\).

The Fréchet mean \(\hat{A}_0\) of the lowest level best fitting subsphere \(\hat{A}_1\) is also determined by this function.

Examples

>>> from pns.pss import pss
>>> from pns.util import unit_sphere, circular_data, circle_3d
>>> x = circular_data([0, -1, 0])
>>> v, r = pss(x)
>>> import matplotlib.pyplot as plt
... ax = plt.figure().add_subplot(projection='3d', computed_zorder=False)
... ax.plot_surface(*unit_sphere(), color='skyblue', alpha=0.6, edgecolor='gray')
... ax.scatter(*x.T, marker="x")
... ax.plot(*circle_3d(v, r), color="tab:orange", zorder=10)
_images/index-9.png

Hypersphere functions#

Basic functions to handle data on a hypersphere.

Conventions for intrinsic coordiates are:
  • xi[…, 0]: azimuthal angle in [-pi, pi]

  • xi[…, 1:]: centered elevation angles in [-pi/2, pi/2]

pns.base.intrinsic_to_extrinsic(xi)[source]#

Convert hypersphere intrinsic coordinates to extrinsic.

Parameters:
xiarray of shape (N, d)

Hyperspherical coordinates.

Returns:
Xarray of shape (N, d+1)

Cartesian coordinates.

Notes

Intrinsic coordiates are:
  • xi[…, 0]: azimuthal angle in [-pi, pi]

  • xi[…, 1:]: centered elevation angles in [-pi/2, pi/2]

Examples

>>> import numpy as np
>>> from pns.base import intrinsic_to_extrinsic
>>> xi = np.array([[0, np.pi/2]])
>>> X = intrinsic_to_extrinsic(xi)
>>> np.isclose(X, [[0, 0, 1]])
array([[ True,  True,  True]])
pns.base.extrinsic_to_intrinsic(X)[source]#

Convert hypersphere extrinsic coordinates to intrinsic.

Parameters:
Xarray of shape (N, d+1)

Cartesian coordinates.

Returns:
xiarray of shape (N, d)

Hyperspherical coordinates.

Examples

>>> from pns.base import extrinsic_to_intrinsic
>>> from pns.util import circular_data
>>> X = circular_data()
>>> X.shape
(100, 3)
>>> extrinsic_to_intrinsic(X).shape
(100, 2)
pns.base.rotation_matrix(v)[source]#

Rotation matrix.

Moves \(v\) to the north pole.

Parameters:
v(m+1) real array

Unit-norm direction vector.

Returns:
(m+1, m+1) array of float64

Rotational matrix.

Notes

This is the function \(R(v_k)\) in the original paper.

Examples

Moving [0, 1, 0] to the north pole, in other words, moving the north pole to [0, -1, 0].

>>> from pns.base import rotation_matrix
>>> from pns.util import unit_sphere, circular_data
>>> X = circular_data()
>>> R = rotation_matrix([0, 1, 0])
>>> X_rotated = X @ R.T
>>> import matplotlib.pyplot as plt
... ax = plt.figure().add_subplot(projection='3d', computed_zorder=False)
... ax.plot_surface(*unit_sphere(), color='skyblue', alpha=0.6, edgecolor='gray')
... ax.scatter(*X.T)
... ax.scatter(*X_rotated.T)
_images/index-10.png
pns.base.exp_map(z)[source]#

Exponential map of hypersphere at (0, 0, …, 0, 1).

Parameters:
z(N, d) real array

Vectors on tangent space.

Returns:
(N, d+1) real array

Points on d-sphere.

pns.base.log_map(x)[source]#

Log map of hypersphere at (0, 0, …, 0, 1).

Parameters:
x(N, d+1) real array

Points on d-sphere.

Returns:
(N, d) real array

Vectors on tangent space.

pns.base.circle_mean(X)[source]#

Frechet mean of data on a circle.

Parameters:
Xreal array of shape (N, 2)
Returns:
meanarray of shape (2,)

Notes

Uses the algorithm [1] implemented by the Geomstats [2] package.

Copyright (c) 2018 Nina Miolane

References

[1]

Hotz, T. and S. F. Huckemann (2015), “Intrinsic means on the circle: Uniqueness, locus and asymptotics”, Annals of the Institute of Statistical Mathematics 67 (1), 177-193. https://arxiv.org/abs/1108.2141

Examples

>>> import numpy as np
>>> from pns.base import circle_mean
>>> t = np.linspace(-np.pi, np.pi / 2, 10)
>>> X = np.stack([np.cos(t), np.sin(t)]).T
>>> mean = circle_mean(X)
>>> import matplotlib.pyplot as plt
... plt.scatter(*X.T, label="data")
... plt.scatter(*mean, label="mean")
... plt.legend()
... plt.gca().set_aspect("equal")
_images/index-11.png

Utilities#

Utility functions to generate and plot sample data.

pns.util.unit_sphere()[source]#

Helper function to plot a unit sphere.

Returns:
x, y, zarray

Coordinates for unit sphere.

Examples

>>> from pns.util import unit_sphere
>>> import matplotlib.pyplot as plt
... ax = plt.figure().add_subplot(projection='3d')
... ax.plot_surface(*unit_sphere(), color='skyblue', alpha=0.6, edgecolor='gray')
_images/index-12.png
pns.util.circular_data(v=(0, 0, 1))[source]#

Circular data on a 3D unit sphere.

Parameters:
varray of shape (3,), default=(0, 0, 1)

Unit vector to center of circle.

Returns:
ndarray of shape (100, 3)

Data coordinates.

Examples

>>> from pns.util import unit_sphere, circular_data
>>> v = [0, -1, 0]
>>> X = circular_data(v)
>>> X.shape
(100, 3)
>>> import matplotlib.pyplot as plt
... ax = plt.figure().add_subplot(projection='3d', computed_zorder=False)
... ax.plot_surface(*unit_sphere(), color='skyblue', alpha=0.6, edgecolor='gray')
... ax.scatter(*X.T)
_images/index-13.png
pns.util.circle_3d(v, theta, n=100)[source]#

Helper function to plot a circle in 3D.

Parameters:
v(3,) array

Unit vector to center of circle in 3D.

thetascalar

Geodesic distance.

nint, default=100

Number of points.

Examples

>>> from pns.util import unit_sphere, circle_3d
>>> circle = circle_3d([0, -1, 0], 0.5)
>>> import matplotlib.pyplot as plt
... ax = plt.figure().add_subplot(projection='3d')
... ax.plot_surface(*unit_sphere(), color='skyblue', alpha=0.6, edgecolor='gray')
... ax.plot(*circle, color="tab:orange", zorder=10)
_images/index-14.png