Source code for ddg.geometry.spheres

"""Module for more abstract sphere interfaces and classes.

For users: Spheres should be created using factory methods of geometries. For
example:

    >>> import ddg
    >>> euc = ddg.geometry.euclidean(3)
    >>> sph = euc.sphere([0, 1, 0, 1], 1.3)
"""
from abc import ABC, abstractmethod
from textwrap import indent
from typing import Any

import numpy as np

from ddg.geometry import quadrics
from ddg.geometry.abc import Embeddable, QuadricConvertible
from ddg.geometry.subspaces import Point, Subspace, coordinate_hyperplane, whole_space

__all__ = [
    "SphereLike",
    "CayleyKleinSphereLike",
    "QuadricSphere",
    "CayleyKleinSphere",
    "MetricCayleyKleinSphere",
    "GeneralizedCayleyKleinSphere",
]


############
# Interfaces
############


[docs]class SphereLike(Embeddable): """Most general sphere interface. Notes ----- Implements the ``in``, ``==`` and ``<=`` relations, defined as set membership "∈", set equality and set containment "⊆" respectively. """ center: Point """Center of the sphere. """ radius: float """ Most often given in terms of a metric, the radius can mean different things depending on the interpretation in a certain geometry. """ subspace: Subspace """A subspace containing `center` that will be intersected with the sphere. """ geometry: Any """Geometry the sphere and in paricular the radius is interpreted in. """ dimension: int """Dimension of the sphere. If the sphere is not a manifold, this is the maximal dimension of a manifold contained in the sphere. """
[docs] @abstractmethod def is_hypersphere(self): """Whether the sphere is a hypersphere (i.e. of maximal dimension). Returns ------- bool """ raise NotImplementedError
[docs] @abstractmethod def is_circle(self): """Whether the sphere is a circle (i.e. 1-dimensional) Returns ------- bool """ raise NotImplementedError
@abstractmethod def __contains__(self, point): raise NotImplementedError @abstractmethod def __eq__(self, other): raise NotImplementedError @abstractmethod def __le__(self, other): raise NotImplementedError @abstractmethod def __repr__(self): raise NotImplementedError @abstractmethod def __str__(self): return repr(self)
[docs]class CayleyKleinSphereLike(SphereLike, QuadricConvertible): """ABC for Cayley-Klein spheres, generalized Cayley-Klein spheres and Metric Cayley-Klein spheres. """ absolute: quadrics.Quadric """Absolute quadric. Always contained in `subspace`. """
[docs] @abstractmethod def metric_radius(self): """Radius in terms of Cayley-Klein metric. This method will fail if the given radius does not correspond to a metric radius. Returns ------- float """ raise NotImplementedError
[docs] @abstractmethod def cayley_klein_radius(self): """Cayley-Klein radius. This method will fail for horospheres. Returns ------- float See also -------- CayleyKleinSphere """ raise NotImplementedError
[docs] @abstractmethod def generalized_radius(self): """Generalized radius. Returns ------- float See also -------- GeneralizedCayleyKleinSphere """ raise NotImplementedError
class _QuadricSphereGeometryBridge(ABC): """QSGB for short. Implementations of this interface are composed with QuadricSpheres to provide all geometry-specific functionality. """ geometry: Any """Geometry related to the bridge. """ model_dimension: int """Dimension of the model space of the geometry. """ @abstractmethod def sphere_to_quadric(self, sphere): """Convert a sphere to a quadric. Parameters ---------- sphere : SphereLike Returns ------- Quadric """ raise NotImplementedError @abstractmethod def validate_sphere(self, sphere): """Validate a sphere. For example, the center has to be valid. Examples of invalid centers: - Points at infinity in Euclidean geometry. - Points not on the unit sphere in spherical geometry. - Points not in `absolute.subspace` for Cayley-Klein spheres etc. Parameters ---------- sphere : Sphere Raises ------ ValueError If sphere is not valid. """ raise NotImplementedError @abstractmethod def embed(self, sphere, subspace=None): """Embed a sphere. Parameters ---------- sphere : SphereLike subspace : Subspace (default=None) Default is the equatorial plane in (n+1)-space. Returns ------- SphereLike See also -------- Embeddable """ raise NotImplementedError @abstractmethod def unembed(self, sphere, subspace=None): """Inverse of `embed`. Parameters ---------- sphere : SphereLike subspace : Subspace (default=None) Default is the equatorial plane. Returns ------- SphereLike See also -------- Embeddable """ raise NotImplementedError ############## # Realizations ##############
[docs]class QuadricSphere(SphereLike, QuadricConvertible): """Implementation of SphereLike for spheres that are also quadrics. For users: Please refer to :py:class:`.SphereLike`. Do not try to instantiate this directly. Parameters ---------- center : Point or numpy.ndarray of shape (n+1,) radius : float geometry_bridge : _QuadricSphereGeometryBridge subspace : Subspace or array_like of shape (m+1, n+1) (default=None) If given as an array, the subspace will be the span of the rows/elements. If None is given, uses whole space with standard basis. atol, rtol : float (default=None) Attributes ---------- center : Point radius : float subspace : Subspace Methods ------- quadric Raises ------ ValueError - If ambient dimension of `subspace` and `center` don't match. - If `center` is not contained in `subspace`. See also -------- SphereLike Notes ----- Everything is implemented by converting to a quadric, then using the corresponding quadric functionality. Geometry-specific functionality that can not be generalized is delegated to a stored QuadricSphereGeometryBridge object. """ # Additional attribute: _geometry_bridge: _QuadricSphereGeometryBridge def __init__( self, center, radius, geometry_bridge, subspace=None, atol=None, rtol=None ): if not isinstance(center, Point): center = Point(center) self.center = center self.radius = radius if subspace is None: subspace = whole_space(self.ambient_dimension) elif np.size(subspace) == 0: subspace = Subspace([0] * (self.ambient_dimension + 1)) else: if not isinstance(subspace, Subspace): subspace = Subspace(*subspace) if subspace.ambient_dimension != self.ambient_dimension: raise ValueError( f"Dimension mismatch. Can not define Sphere in " f"{self.ambient_dimension}-dimensional space with Subspace in " f"{subspace.ambient_dimension}-dimensional space." ) self.subspace = subspace if self.center not in self.subspace: raise ValueError("Center of sphere is not in containing subspace.") self._geometry_bridge = geometry_bridge self._geometry_bridge.validate_sphere(self) self.atol = atol self.rtol = rtol @property def ambient_dimension(self): return self.center.ambient_dimension @property def dimension(self): return self.quadric().dimension @property def geometry(self): return self._geometry_bridge.geometry
[docs] def quadric(self): return self._geometry_bridge.sphere_to_quadric(self)
[docs] def is_hypersphere(self): return self.dimension == self._geometry_bridge.model_dimension - 1
[docs] def is_circle(self): return self.dimension == 1
[docs] def at_infinity(self): return self.quadric().at_infinity()
[docs] def embed(self, subspace=None): return self._geometry_bridge.embed(self, subspace)
[docs] def unembed(self, subspace=None): return self._geometry_bridge.unembed(self, subspace)
def __contains__(self, point): return point in self.quadric() def __eq__(self, other): return self.quadric() == other.quadric() def __le__(self, other): return self.quadric() <= other.quadric() def __repr__(self): argstrings = [] argstrings.append(repr(self.center)) argstrings.append(repr(self.radius)) argstrings.append(f"geometry_bridge={self._geometry_bridge!r}") argstrings.append(f"subspace={self.subspace!r}") argstrings.append(f"atol={self.atol!r}") argstrings.append(f"rtol={self.rtol!r}") return self.__class__.__name__ + "(" + ", ".join(argstrings) + ")" def __str__(self): # It might be a good idea to implement this through the bridge. We can # still do this later. sections = [] if self.is_circle(): name = "Circle" # 2-spheres in 3-space are usually just called spheres. I think this is an # important enough special case to be treated specially. elif self.ambient_dimension == 3 and self.is_hypersphere(): name = "Sphere" elif self.is_hypersphere(): name = "Hypersphere" else: name = f"{self.dimension}-sphere" # We put str(self.geometry) in its own indented block on a new line # because it could contain newlines. sections.append( f"{name} in geometry/model:\n" + indent(str(self.geometry), " ") ) sections.append(f"Center: {self.center.point}") sections.append(f"Radius: {self.radius}") if self.subspace.codimension > 0: sections.append( "Containing subspace basis (columns):\n" + indent(str(self.subspace.matrix), " ") ) return "\n".join(sections)
class _CayleyKleinQSGB(_QuadricSphereGeometryBridge): """_QuadricSphereGeometryBridge for Cayley-Klein spheres. Parameters ---------- absolute : Quadric geometry : MetricCayleyKleinGeometry Used to convert between metric and Cayley-Klein radii. This is only mandatory for MetricCayleyKleinSphere, otherwise you can set this to None. """ def __init__(self, absolute, geometry): self.absolute = absolute self.geometry = geometry def validate_sphere(self, sphere): if sphere.center not in self.absolute.subspace: raise ValueError( "Not a valid center. Centers of Cayley-Klein spheres must be " "in absolute.subspace." ) if isinstance(sphere, MetricCayleyKleinSphere): if sphere.center in self.absolute: raise ValueError( "Not a valid center: Centers of metric Cayley-Klein " "spheres can not lie on the absolute." ) @property def model_dimension(self): return self.absolute.subspace.dimension @staticmethod def sphere_to_quadric(sphere): Q = quadrics.generalized_cayley_klein_sphere( sphere.center, sphere.generalized_radius(), sphere.absolute ) Q.atol = sphere.atol Q.rtol = sphere.rtol return Q @staticmethod def embed(sphere, subspace=None): if subspace is None: subspace = coordinate_hyperplane(sphere.ambient_dimension + 1) return type(sphere)( sphere.center.embed(subspace), sphere.radius, absolute=sphere.absolute.embed(subspace), geometry=sphere.geometry, subspace=sphere.subspace.embed(subspace), ) @staticmethod def unembed(sphere, subspace=None): if subspace is None: subspace = coordinate_hyperplane(sphere.ambient_dimension) return type(sphere)( sphere.center.unembed(subspace), sphere.radius, absolute=sphere.absolute.unembed(subspace), geometry=sphere.geometry, subspace=sphere.subspace.unembed(subspace), ) def __repr__(self): return f"_CayleyKleinQSGB({self.absolute!r}, {self.geometry!r})" class _CayleyKleinSphereIntermediate(QuadricSphere, CayleyKleinSphereLike): # Just an intermediate class to implement some behavior that is common to # all three main Cayley-Klein sphere types. def __init__( self, center, radius, absolute, geometry, subspace=None, atol=None, rtol=None ): geometry_bridge = _CayleyKleinQSGB(absolute, geometry) super().__init__( center, radius, subspace=subspace, geometry_bridge=geometry_bridge, atol=atol, rtol=rtol, ) @property def absolute(self): """The absolute quadric. Returns ------- Quadric """ return quadrics.intersect_quadric_subspace( self._geometry_bridge.absolute, self.subspace ) def __repr__(self): argstrings = [] argstrings.append(repr(self.center)) argstrings.append(repr(self.radius)) argstrings.append(f"absolute={self.absolute!r}") argstrings.append(f"geometry={self.geometry!r}") argstrings.append(f"subspace={self.subspace!r}") argstrings.append(f"atol={self.atol!r}") argstrings.append(f"rtol={self.rtol!r}") return self.__class__.__name__ + "(" + ", ".join(argstrings) + ")"
[docs]class CayleyKleinSphere(_CayleyKleinSphereIntermediate): """Cayley-Klein sphere. The radius is not given in terms of a metric, but in terms of the Cayley-Klein "distance" induced by the `absolute`. More precisely: This "sphere" is the set of all points `x` where :: b(x, c) ** 2 - r * b(c, c) * b(x, x) = 0 where `c` is `center.point`, `r` is the radius and `b` is `absolute.inner_product`.` This formulation allows for points on the absolute to be included. Parameters ---------- center : Point or numpy.ndarray of shape (n+1,) center given as Point or in homogeneous coordinates. radius : float absolute : Quadric geometry : MetricCayleyKleinGeometry (default=None) This geometry, if provided, will be used to convert between Cayley-Klein radius and metric radius. subspace : Subspace or list of numpy.ndarray of shape (n+1,) (default=None) If given as list, the elements of the list will be interpreted as points in homogeneous coordinates spanning the subspace. atol, rtol : float (default=None) Attributes ---------- center : Point radius : float Cayley-Klein radius. subspace : Subspace Methods ------- quadric metric_radius cayley_klein_radius generalized_radius See also -------- SphereLike, CayleyKleinSphereLike """ def __init__( self, center, radius, absolute, geometry=None, subspace=None, atol=None, rtol=None, ): # Give geometry a default of None to indicate it's not required. super().__init__( center, radius, absolute=absolute, geometry=geometry, subspace=subspace, atol=atol, rtol=rtol, )
[docs] def metric_radius(self): """Radius in terms of Cayley-Klein metric. This method will fail if the given radius does not correspond to a metric radius. Returns ------- float Raises ------ AttributeError If no `geometry` to convert between metric and Cayley-Klein distance was provided. """ if self.geometry is None: raise AttributeError( "No MetricCayleyKleinGeometry to convert between metric and " "Cayley-Klein distance was provided." ) return self.geometry.cayley_klein_distance_to_metric(self.cayley_klein_radius())
[docs] def cayley_klein_radius(self): return self.radius
[docs] def generalized_radius(self): r = self.cayley_klein_radius() c = self.center.point b = self.absolute.inner_product return r * b(c, c)
def __str__(self): sections = [] # We put str(self.geometry) in its own indented block on a new line # because it could contain newlines. if self.geometry is None: sections.append( "Cayley-Klein sphere with absolute:\n" + indent(str(self.absolute), " ") ) else: sections.append( "Cayley-Klein sphere in geometry/model:\n" + indent(str(self.geometry), " ") ) sections.append(f"Center: {self.center.point}") sections.append(f"Cayley-Klein radius: {self.radius}") if self.subspace.codimension > 0: sections.append( "Containing subspace basis (columns):\n" + indent(str(self.subspace.matrix), " ") ) return "\n".join(sections)
[docs]class MetricCayleyKleinSphere(_CayleyKleinSphereIntermediate): """Cayley-Klein sphere with radius given in terms of metric. Parameters ---------- center : Point or numpy.ndarray of shape (n+1,) center given as Point or in homogeneous coordinates. radius : float absolute : Quadric geometry : MetricCayleyKleinGeometry This geometry will be used to convert between Cayley-Klein radius and metric radius. subspace : Subspace or list of numpy.ndarray of shape (n+1,) (default=None) If given as list, the elements of the list will be interpreted as points in homogeneous coordinates spanning the subspace. atol, rtol : float (default=None) Attributes ---------- center : Point radius : float Radius in terms of Cayley-Klein metric. subspace : Subspace Methods ------- quadric metric_radius cayley_klein_radius generalized_radius See also -------- SphereLike, CayleyKleinSphereLike """
[docs] def metric_radius(self): return self.radius
[docs] def cayley_klein_radius(self): return self.geometry.metric_to_cayley_klein_distance(self.metric_radius())
[docs] def generalized_radius(self): r = self.cayley_klein_radius() c = self.center.point b = self.absolute.inner_product return r * b(c, c)
[docs]class GeneralizedCayleyKleinSphere(_CayleyKleinSphereIntermediate): """Generalized Cayley-Klein sphere The radius is given neither in terms of a metric nor a Cayley-Klein distance. This "sphere" is the set of all points `x` where :: b(x, c) ** 2 - r * b(x, x) = 0 where `c` is `center.point`, `r` is the radius and `b` is `absolute.inner_product`. This definition allows for horospheres, i.e. spheres with a center that lies on the absolute quadric, at the expense of depending on the representative of `center`. Parameters ---------- center : Point or numpy.ndarray of shape (n+1,) center given as Point or in homogeneous coordinates. radius : float absolute : Quadric geometry : MetricCayleyKleinGeometry (default=None) This geometry, if provided, will be used to convert between Cayley-Klein radius and metric radius. subspace : Subspace or list of numpy.ndarray of shape (n+1,) (default=None) If given as list, the elements of the list will be interpreted as points in homogeneous coordinates spanning the subspace. atol, rtol : float (default=None) Attributes ---------- center : Point radius : float Generalized radius. subspace : Subspace Methods ------- quadric metric_radius cayley_klein_radius generalized_radius See also -------- SphereLike, CayleyKleinSphereLike """ def __init__( self, center, radius, absolute, geometry=None, subspace=None, atol=None, rtol=None, ): # Give geometry a default of None to indicate it's not required. super().__init__( center, radius, absolute=absolute, geometry=geometry, subspace=subspace, atol=atol, rtol=rtol, )
[docs] def metric_radius(self): """Radius in terms of Cayley-Klein metric. This method will fail if the given radius does not correspond to a metric radius. Returns ------- float Raises ------ AttributeError If no `geometry` to convert between metric and Cayley-Klein distance was provided. """ if self.geometry is None: raise AttributeError( "No MetricCayleyKleinGeometry to convert between metric and " "Cayley-Klein distance was provided." ) return self.geometry.cayley_klein_distance_to_metric(self.cayley_klein_radius())
[docs] def cayley_klein_radius(self): """Cayley-Klein radius. This method will fail for horospheres. Returns ------- float Raises ------ ValueError If `center` is contained in `absolute`, so the Cayley-Klein radius can not be computed. See also -------- CayleyKleinSphere """ if self.center in self.absolute: raise ValueError( "This is a horosphere, so the Cayley-Klein radius can not be " "computed." ) r = self.radius c = self.center.point b = self.absolute.inner_product return r / b(c, c)
[docs] def generalized_radius(self): return self.radius
def __str__(self): sections = [] # We put str(self.geometry) in its own indented block on a new line # because it could contain newlines. if self.geometry is None: sections.append( "Generalized Cayley-Klein sphere with absolute:\n" + indent(str(self.absolute), " ") ) else: sections.append( "Generalized Cayley-Klein sphere in geometry/model:\n" + indent(str(self.geometry), " ") ) sections.append(f"Center: {self.center.point}") sections.append(f"Generalized radius: {self.radius}") if self.subspace.codimension > 0: sections.append( "Containing subspace basis (columns):\n" + indent(str(self.subspace.matrix), " ") ) return "\n".join(sections)