"""Möbius geometry module.
Contains model classes and functions for conversion between the models.
"""
import numpy as np
import ddg
from ddg.geometry._intersection import meet
from ddg.geometry._projections import (
inverse_stereographic_project,
stereographic_project,
)
from ddg.geometry._quadrics import Quadric
from ddg.geometry.geometry_model_templates import CayleyKleinGeometry, MetricGeometry
from ddg.math.symmetric_matrices import symmetric_matrix_from_diagonal
__all__ = [
"ProjectiveModel",
"ParaboloidModel",
"EuclideanModel",
"euclidean_to_projective",
"projective_to_euclidean",
"paraboloid_to_projective_and_back",
]
[docs]class EuclideanModel:
"""Euclidean model of Möbius geometry.
.. rubric:: Model space
The model space is Euclidean space together with a single point at
infinity, which we represent by ``float('inf')``.
.. rubric:: Representation of objects
Möbius Spheres in this model are Euclidean spheres or subspaces (spheres
with center at infinity).
"""
class _Template(CayleyKleinGeometry):
"""
Common superclass for sphere model and paraboloid model, just to avoid
duplicate code.
Subclasses just need to have an attribute/property `absolute` and it will be used in
the computations defined in the template methods of this auxiliary class.
"""
def angle(self, s1, s2):
"""Angle between two Möbius spheres.
Parameters
----------
s1, s2 : ddg.geometry.Quadric
Möbius spheres.
Returns
-------
float
"""
s1 = self.pole_of_sphere(s1)
s2 = self.pole_of_sphere(s2)
arg = np.sqrt(self.cayley_klein_distance(s1, s2))
if ddg.nonexact.isclose(arg, 1):
return 0.0
return np.arccos(arg)
def sphere_from_pole(self, subspace) -> Quadric:
"""Get a Möbius sphere from its pole.
Returns intersection of `Q` and `Q.polarize(subspace)`, where `Q` is
the Möbius quadric.
Parameters
----------
subspace : ddg.geometry.Subspace
Returns
-------
ddg.geometry.Quadric
"""
Q = self.absolute
return meet(Q, Q.polarize(subspace))
def pole_of_sphere(self, sphere):
"""Return pole corresponding to sphere.
Parameters
----------
sphere : ddg.geometry.Quadric
Returns
-------
ddg.geometry.Subspace
"""
return self.absolute.polarize(sphere.subspace)
def __contains__(self, point):
return point in self.absolute
[docs]class ProjectiveModel(_Template):
"""Projective model of Möbius geometry.
.. rubric:: Model space
The Möbius quadric is the quadric with matrix ``diag([1,...,1, -1])``,
which can be thought of as the unit sphere.
.. rubric:: Representation of objects
Spheres in this model are represented by quadrics which are intersections of
the Möbius quadric with a subspace.
Parameters
----------
dimension : int
Attributes
----------
dimension : int
"""
@property
def absolute(self):
"""The absolute quadric with matrix ``diag([1,...,1, -1])``.
Returns
-------
ddg.geometry.Quadric
"""
matrix = np.eye(self.dimension + 2)
matrix[-1, -1] = -1
return Quadric(matrix)
[docs] def paraboloid(self):
"""Corresponding paraboloid model of Möbius geometry."""
return ParaboloidModel(self.dimension)
[docs] def from_paraboloid(self, object_):
"""Alias for :py:func:`.paraboloid_to_projective_and_back`"""
return paraboloid_to_projective_and_back(object_, embedded=embedded)
@property
def euclidean_point(self):
p = np.zeros(self.ambient_dimension + 1)
p[[-2, -1]] = 0.5
return ddg.geometry.Point(p)
@property
def euclidean_subspace(self):
return ddg.geometry.coordinate_hyperplane(self.ambient_dimension)
@property
def elliptic_subspace(self):
return self.absolute.polarize(self.hyperbolic_point)
[docs] def to_paraboloid(self, object_):
"""Alias for :py:func:`.paraboloid_to_projective_and_back`"""
return paraboloid_to_projective_and_back(object_, embedded=embedded)
[docs] def euclidean(self):
"""Corresponding projective model of Euclidean geometry."""
return ddg.geometry.euclidean_models.ProjectiveModel(self.dimension)
[docs] def from_euclidean(self, object_, embedded=False):
"""Alias for :py:func:`.euclidean_models.projective_to_moebius`"""
return ddg.geometry.euclidean_models.projective_to_moebius(
object_, embedded=embedded
)
[docs] def to_euclidean(self, object_, embedded=False):
"""Alias for :py:func:`.euclidean_models.moebius_to_projective`"""
return ddg.geometry.euclidean_models.moebius_to_projective(
object_, embedded=embedded
)
@property
def hyperbolic_point(self):
p = np.zeros(self.ambient_dimension + 1)
p[-2] = 1
return ddg.geometry.Point(p)
@property
def hyperbolic_subspace(self):
return self.absolute.polarize(self.hyperbolic_point)
[docs] def hyperbolic(self):
"""Corresponding projective model of hyperbolic geometry."""
return ddg.geometry.hyperbolic_models.ProjectiveModel(self.dimension)
[docs] def to_hyperbolic(self, object_, embedded=False):
"""Alias for :py:func:`.hyperbolic_models.hemisphere_to_projective`"""
return ddg.geometry.hyperbolic_models.hemisphere_to_projective(
object_, embedded=embedded
)
[docs] def from_hyperbolic(self, object_, embedded=False):
"""Alias for :py:func:`.hyperbolic_models.projective_to_hemisphere`"""
return ddg.geometry.hyperbolic_models.projective_to_hemisphere(
object_, embedded=embedded
)
[docs] def hyperbolic_poincare(self):
"""Corresponding Poincare disk model of hyperbolic geometry."""
return ddg.geometry.hyperbolic_models.PoincareDiskModel(self.dimension)
[docs] def to_hyperbolic_poincare(self, object_, embedded=False):
"""Alias for :py:func:`.hyperbolic_models.projective_to_poincare`"""
return ddg.geometry.hyperbolic_models.projective_to_poincare(
object_, embedded=embedded
)
[docs] def from_hyperbolic_poincare(self, object_, embedded=False):
"""Alias for :py:func:`.hyperbolic_models.poincare_to_projective`"""
return ddg.geometry.hyperbolic_models.poincare_to_projective(
object_, embedded=embedded
)
[docs] def hyperbolic_half_space(self):
"""Corresponding Poincare disk model of hyperbolic geometry."""
return ddg.geometry.hyperbolic_models.PoincareDiskModel(self.dimension)
[docs] def to_hyperbolic_half_space(self, object_, embedded=False):
"""Alias for :py:func:`.hyperbolic_models.hemisphere_to_half_space`"""
return ddg.geometry.hyperbolic_models.hemisphere_to_half_space(
object_, embedded=embedded
)
[docs] def from_hyperbolic_half_space(self, object_, embedded=False):
"""Alias for :py:func:`.hyperbolic_models.half_space_to_hemisphere`"""
return ddg.geometry.hyperbolic_models.half_space_to_hemisphere(
object_, embedded=embedded
)
@property
def elliptic_point(self):
p = np.zeros(self.ambient_dimension + 1)
p[-1] = 1
return ddg.geometry.Point(p)
@property
def elliptic_subspace(self):
return self.absolute.polarize(self.elliptic_point)
[docs] def elliptic(self):
"""Corresponding projective model of elliptic geometry."""
return ddg.geometry.elliptic_models.ProjectiveModel(self.dimension)
[docs] def from_elliptic(self, object_, embedded=False):
"""Alias for :py:func:`.elliptic_models.projective_to_moebius`"""
return ddg.geometry.elliptic_models.projective_to_moebius(
object_, embedded=embedded
)
[docs] def to_elliptic(self, object_, embedded=False):
"""Alias for :py:func:`.elliptic_models.moebius_to_projective`"""
return ddg.geometry.elliptic_models.moebius_to_projective(
object_, embedded=embedded
)
[docs] def lie(self):
"""Corresponding projective model of Lie geometry."""
return ddg.geometry.lie_models.ProjectiveModel(self.dimension)
[docs] def from_lie(self, object_, embedded=False):
raise NotImplementedError
[docs] def to_lie(self, object_, embedded=False):
raise NotImplementedError
def __str__(self):
return f"Projective model of {self.dimension}D Möbius geometry"
[docs]class ParaboloidModel(_Template):
"""Paraboloid model of Möbius geometry.
.. rubric:: Model space
The Möbius quadric is the quadric with matrix::
I |
--+-----
| 0 1
| 1 0
Which can be thought of as a paraboloid.
.. rubric:: Representation of objects
Spheres in this model are represented by quadrics which are intersections of
the Möbius quadric with a subspace.
Parameters
----------
dimension : int
Attributes
----------
dimension : int
"""
@property
def absolute(self):
"""The absolute quadric.
Returns the quadric with matrix ::
I |
--+-----
| 0 1
| 1 0
Returns
-------
ddg.geometry.Quadric
"""
diagonal = np.ones(self.dimension + 2)
diagonal[[-2, -1]] = 0
return Quadric(symmetric_matrix_from_diagonal(diagonal, parabolic=True))
def __str__(self):
return f"Projective paraboloid model of {self.dimension}D Möbius geometry"
class _ProjectiveSubgeometry(ProjectiveModel, MetricGeometry):
"""Common superclass for submodels of projective Möbius geometry:
hyperbolic_models.HemisphereModel, euclidean_models.MoebiusModel and
spherical_models.ProjectiveModel.
"""
pass
[docs]def euclidean_to_projective(object_, embedded=False):
r"""Convert from Euclidean model to projective model.
This function works by embedding into the equatorial plane, then projecting
stereographically.
Parameters
----------
object_ : ddg.geometry.Subspace or ddg.geometry.spheres.SphereLike
Object in n-dimensional ambient space.
embedded : bool (default=False)
If False, `object_` is embedded into the equatorial plane before
projecting.
Returns
-------
ddg.geometry.Quadric
An intersection of the Möbius quadric with a subspace in
(n+1)-dimensonal ambient space.
"""
if not embedded:
object_ = object_.embed()
return inverse_stereographic_project(object_)
[docs]def projective_to_euclidean(object_, embedded=False):
r"""Convert a Möbius sphere to a Euclidean sphere or subspace.
This function works by projecting stereographically to the
equatorial plane, then computing coordinates.
Parameters
----------
object_ : ddg.geometry.Quadric
Intersection of Möbius quadric with a subspace, all in (n+1)-dim.
ambient space.
embedded : bool (default=False)
Whether to return the object in the equatorial plane or in n-dim. ambient space.
Returns
-------
ddg.geometry.spheres.SphereLike or ddg.geometry.Subspace
"""
mob = ddg.geometry.euclidean_models.MoebiusModel(object_.ambient_dimension - 1)
img = stereographic_project(object_)
if not embedded:
img = img.unembed()
if (
isinstance(object_, Quadric)
and object_ <= mob.absolute
and mob.fixed_point not in object_.subspace
):
if mob.absolute.signature(object_.subspace).is_degenerate:
# If the quadric only consists of a single point
img = ddg.geometry.quadric_to_subspaces(img)[0]
else:
img = ddg.geometry.euclidean_models.ProjectiveModel.quadric_to_sphere(img)
return img
[docs]def paraboloid_to_projective_and_back(object_):
r"""Convert between projective and paraboloid models.
Transform an object with the transformation that takes the projective model
quadric to the paraboloid model quadric. This transformation is an
involution, i.e. applying it again is the same as undoing the
transformation. The transformation is ::
I |
---+---------------------
| 1/sqrt(2) 1/sqrt(2)
| 1/sqrt(2) -1/sqrt(2)
Where I is the identity matrix of the appropriate size.
Warnings
--------
This WILL mutate `object_`.
Parameters
----------
object_ : Transformable
Returns
-------
object_ : type(object\_)
Transformed `object_`.
"""
trafo = np.eye(object_.ambient_dimension + 1)
trafo[-2:, -2:] = np.sqrt(0.5) * np.array([[1, 1], [1, -1]])
object_ = object_.transform(trafo)
return object_