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._subspaces import Point, Subspace, coordinate_hyperplane, whole_space
from ddg.geometry.abc import Embeddable, QuadricConvertible

__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 -------- ddg.geometry.spheres.CayleyKleinSphere """ raise NotImplementedError
[docs] @abstractmethod def generalized_radius(self): """Generalized radius. Returns ------- float See also -------- ddg.geometry.spheres.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 : ddg.geometry.spheres.SphereLike Returns ------- ddg.geometry.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 : ddg.geometry.spheres.SphereLike Raises ------ ValueError If sphere is not valid. """ raise NotImplementedError @abstractmethod def embed(self, sphere, subspace=None): """Embed a sphere. Parameters ---------- sphere : ddg.geometry.spheres.SphereLike subspace : ddg.geometry.Subspace (default=None) Default is the equatorial plane in (n+1)-space. Returns ------- ddg.geometry.spheres.SphereLike See also -------- Embeddable """ raise NotImplementedError @abstractmethod def unembed(self, sphere, subspace=None): """Inverse of `embed`. Parameters ---------- sphere : ddg.geometry.spheres.SphereLike subspace : ddg.geometry.Subspace (default=None) Default is the equatorial plane. Returns ------- ddg.geometry.spheres.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 : ddg.geometry.Point or numpy.ndarray of shape (n+1,) radius : float geometry_bridge : ddg.geometry.spheres._QuadricSphereGeometryBridge subspace : ddg.geometry.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 : ddg.geometry.Point radius : float subspace : ddg.geometry.Subspace Methods ------- quadric Raises ------ ValueError - If ambient dimension of `subspace` and `center` don't match. - If `center` is not contained in `subspace`. See also -------- ddg.geometry.spheres.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:\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 : ddg.geometry.Quadric geometry : ddg.geometry.spheres.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 ------- ddg.geometry.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 : ddg.geometry.Point or numpy.ndarray of shape (n+1,) center given as Point or in homogeneous coordinates. radius : float absolute : ddg.geometry.Quadric geometry : ddg.geometry.spheres.MetricCayleyKleinGeometry (default=None) This geometry, if provided, will be used to convert between Cayley-Klein radius and metric radius. subspace : ddg.geometry.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 : ddg.geometry.Point radius : float Cayley-Klein radius. subspace : ddg.geometry.Subspace Methods ------- quadric metric_radius cayley_klein_radius generalized_radius See also -------- ddg.geometry.spheres.SphereLike, ddg.geometry.spheres.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:\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 : ddg.geometry.Point or numpy.ndarray of shape (n+1,) center given as Point or in homogeneous coordinates. radius : float absolute : ddg.geometry.Quadric geometry : ddg.geometry.spheres.MetricCayleyKleinGeometry This geometry will be used to convert between Cayley-Klein radius and metric radius. subspace : ddg.geometry.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 : ddg.geometry.Point radius : float Radius in terms of Cayley-Klein metric. subspace : ddg.geometry.Subspace Methods ------- quadric metric_radius cayley_klein_radius generalized_radius See also -------- ddg.geometry.spheres.SphereLike, ddg.geometry.spheres.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 : ddg.geometry.Point or numpy.ndarray of shape (n+1,) center given as Point or in homogeneous coordinates. radius : float absolute : ddg.geometry.Quadric geometry : ddg.geometry.spheres.MetricCayleyKleinGeometry (default=None) This geometry, if provided, will be used to convert between Cayley-Klein radius and metric radius. subspace : ddg.geometry.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 : ddg.geometry.Point radius : float Generalized radius. subspace : ddg.geometry.Subspace Methods ------- quadric metric_radius cayley_klein_radius generalized_radius See also -------- ddg.geometry.spheres.SphereLike, ddg.geometry.spheres.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 -------- ddg.geometry.spheres.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:\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)