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