"""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)