from textwrap import indent
import re
import numpy as np
from ddg.math import inner_product
import ddg.math.projective as pmath
from ddg.geometry import quadrics, subspaces
[docs]class Geometry:
"""Basic geometry class.
Predefined geometries and geometries that are named upon initialization can
be retrieved with get_geometry.
Parameters
----------
name : str (default=None)
Name of the geometry. Note that geometries can only be retrieved with
get_geometry, if its name is not None.
dimension : int (default=None)
Dimension of the geometry. This is the ambient dimension of the space
the model for the geometry lives in.
dimension_offset : int (default=None)
Offset to subtract from `dimension` to get the 'interpreted' dimension.
For example, Möbius geometry has an offset of 1: The model space of
n-dimensional Möbius geometry is an n-dimensional quadric surface in
(n+1)-dimensional space.
Attributes
----------
name : str
Name of the geometry.
dimension : int
Dimension of the geometry.
dimension_offset : int
name_dimension : int
Equal to dimension - dimension_offset.
Methods
-------
set_alias
See Also
--------
get_geometry
"""
_geometries = {}
# This is realized as a class variable because we want to access it
# before initializing a geometry in get_geometry.
dimension_offset = 0
"""Dimension offset, see above.
This will be overwritten if a `dimension_offset` is passed to a geometry
upon creation.
"""
def __init__(self, name=None, dimension=None, dimension_offset=None):
if not (name is None):
identifier = (name.lower(), dimension)
if self._geometries.get(identifier) is None:
self._geometries[identifier] = self
else:
msg = (f'Geometry with name {name} and dimension {dimension} '
f'already exists. Use get_geometry to retrieve it.')
raise Exception(msg)
self.name = name.lower()
else:
self.name = name
self.dimension = dimension
if dimension_offset is not None:
self.dimension_offset = dimension_offset
if self.dimension is not None:
self.name_dimension = self.dimension - self.dimension_offset
else:
self.name_dimension = None
def __repr__(self):
argstrings = []
argstrings.append(f"name={self.name!r}")
argstrings.append(f"dimension={self.dimension}")
if self.dimension_offset != 0:
argstrings.append(f"dimension_offset={self.dimension_offset}")
return f"{self.__class__.__name__}(" + ', '.join(argstrings) + ")"
def __str__(self):
if not (self.name is None):
temp = self.name.split()
# check for dimension in string
tempbool = not all([i.isdigit() for i in temp[-1][:-1]])
tempbool |= (temp[-1][-1] != 'd') or (len(temp[-1]) == 1)
if len(temp) == 1 or tempbool:
if self.dimension is None:
return f'{self.name} geometry'
return f'{self.dimension}D {self.name} geometry'
else:
dim = temp[-1]
return f"{dim.upper()} {' '.join(temp[:-1])} geometry"
else:
if self.dimension is None:
return 'geometry'
return f'{self.dimension}D geometry'
[docs] def set_alias(self, name, overwrite=False):
"""Set alias for geometry.
A geometry can also be retrieved by its alias using get_geometry.
Parameters
----------
name : str
New alias for the geometry.
overwrite : bool (default=False)
If set to True, the internal name of the geometry will be
overwritten.
"""
if overwrite:
self.name = name
self._geometries[(name.lower(), self.dimension)] = self
[docs]class ProjectiveGeometry(Geometry):
"""Projective geometry class.
Describes an n-dimensional projective space (or its dual space).
Parameters
----------
name : str (default=None)
Name of the geometry. Note that geometries can only be retrieved with
get_geometry, if its name is not None.
dimension : int (default=None)
Dimension of the projective space.
is_dual : bool (default=False)
Indicates whether the geometry describes the primal or dual projective
space.
Attributes
----------
name : str
Name of the geometry.
dimension : int
Dimension of the projective space.
is_dual : bool
True if geometry is dual, else False.
dual : ProjectiveGeometry
The dual geometry.
Methods
-------
set_alias
Set alias for the geometry.
See Also
--------
get_geometry
"""
def __init__(self, name='projective', dimension=None,
dimension_offset=None, is_dual=False):
self.is_dual = is_dual
if name is not None:
name = name.lower()
name = _remove_word_from_string(name, 'dual')
if self.is_dual:
super().__init__(name='dual '+name, dimension=dimension,
dimension_offset=dimension_offset)
else:
super().__init__(name=name, dimension=dimension,
dimension_offset=dimension_offset)
else:
super().__init__(name=name, dimension=dimension,
dimension_offset=dimension_offset)
[docs] def dualize(self):
try:
return ProjectiveGeometry(name=self.name, dimension=self.dimension,
dimension_offset=self.dimension_offset,
is_dual=(not self.is_dual))
except:
if self.is_dual:
dual_name = _remove_word_from_string(self.name, 'dual')
else:
dual_name = 'dual ' + self.name
return get_geometry(dual_name, self.dimension)
[docs] def set_alias(self, name, overwrite=False):
name = name.lower()
name = _remove_word_from_string(name, 'dual')
self._set_alias(name, overwrite=overwrite)
self.dualize()._set_alias(name, overwrite=overwrite)
def _set_alias(self, name, overwrite=False):
if self.is_dual:
super().set_alias('dual ' + name, overwrite=overwrite)
else: super().set_alias(name, overwrite=overwrite)
def __repr__(self):
argstrings = []
argstrings.append(f"name={self.name!r}")
argstrings.append(f'dimension={self.dimension}')
if self.dimension_offset != 0:
argstrings.append(f"dimension_offset={self.dimension_offset}")
if self.is_dual:
argstrings.append(f'is_dual={self.is_dual}')
return 'ProjectiveGeometry(' + ', '.join(argstrings) + ')'
[docs]class CayleyKleinGeometry(ProjectiveGeometry):
"""Cayley-Klein geometry class.
Describes an n-dimensional Cayley-Klein space using an absolute quadric,
or a (pseudo-)conformal geometry on that quadric.
Parameters
----------
name : str (default=None)
Name of the geometry. Note that geometries can only be retrieved with
get_geometry, if its name is not None.
dimension : int (default=None)
Dimension of the projective space.
This is the ambient dimension of the absolute quadric.
is_dual : bool (default=False)
Indicates whether the geometry describes the primal or dual projective
space.
quadric : ddg.Quadric, numpy.array, callable
Absolute quadric of the geometry.
When given as an array it is taken as the symmetric matrix of the
absolute quadric.
When given as a callable inner product, the
parameter dimension becomes mandatory.
Attributes
----------
name : str
Name of the geometry.
dimension : int
Dimension of the projective space.
This is the ambient dimension of the absolute quadric.
is_dual : bool
True if geometry is dual, else False
dual : CayleyKleinGeometry
The dual geometry.
Methods
-------
set_alias
Set alias for the geometry.
inner_product
Inner product of the geometry, if it exists.
d
Distance function
See Also
--------
get_geometry
"""
def __init__(self, quadric, name=None, dimension=None, is_dual=False):
if isinstance(quadric, np.ndarray):
quadric = quadrics.Quadric(quadric)
elif callable(quadric):
temp = inner_product.get_matrix(quadric, dimension+1)
quadric = quadrics.Quadric(temp)
self.quadric = quadric
if dimension is None:
dimension = self.quadric.ambient_dimension
super().__init__(name=name, dimension=dimension, is_dual=is_dual)
[docs] def inner_product(self, v, w):
"""Inner product induced by the absolute quadric.
See also
--------
ddg.geometry.quadrics.Quadric.inner_product
"""
return self.quadric.inner_product(v,w)
[docs] def cayley_klein_distance(self, v, w):
"""Cayley-Klein "distance" induced by the absolute quadric.
See also
--------
ddg.geometry.quadrics.Quadric.cayley_klein_distance
"""
return self.quadric.cayley_klein_distance(v, w)
[docs] def metric_to_cayley_klein_distance(self, d):
"""Return Metric distance converted to Cayley-Klein "distance".
Parameters
----------
d : float
Returns
-------
K : float
"""
raise NotImplementedError
[docs] def cayley_klein_distance_to_metric(self, K):
"""Return Cayley-Klein distance converted to actual metric distance.
Parameters
----------
K : float
Returns
-------
d : float
"""
raise NotImplementedError
[docs] def d(self, v, w):
"""Distance function.
The default implementation attempts to use
`cayley_klein_distance_to_metric` (an abstract method) to convert the
Cayley-Klein distance, wich is always defined in the same way, to a
metric distance.
Parameters
----------
v, w : numpy.ndarray or Point
Homogeneous coordinate vectors or Point instances. Both points
must be either inside or outside the absolute quadric.
Returns
-------
float
"""
K = self.cayley_klein_distance(v, w)
return self.cayley_klein_distance_to_metric(K)
[docs] def dualize(self):
try:
return CayleyKleinGeometry(self.quadric.dualize(), name=self.name,
dimension=self.dimension,
is_dual=(not self.is_dual))
except:
if self.is_dual:
dual_name = _remove_word_from_string(self.name, 'dual')
else:
dual_name = 'dual ' + self.name
return get_geometry(dual_name, self.dimension)
def __repr__(self):
argstrings = []
argstrings.append(repr(self.quadric))
if self.name is not None:
argstrings.append(f"name={self.name!r}")
if self.dimension != self.quadric.ambient_dimension:
argstrings.append(f'dimension={self.dimension}')
if self.is_dual:
argstrings.append(f'is_dual={self.is_dual}')
argstrings = ',\n'.join(argstrings)
return 'CayleyKleinGeometry(\n' + indent(argstrings, ' ') + '\n)'
def __str__(self):
if not (self.name is None):
temp = self.name.split()
# check for dimension in string
tempbool = not all([i.isdigit() for i in temp[-1][:-1]])
tempbool |= (temp[-1][-1] != 'd') or (len(temp[-1]) == 1)
if len(temp) == 1 or tempbool:
if self.dimension is None:
return self.name + ' geometry'
return (str(self.dimension) + 'D ' + self.name + ' geometry')
else:
dim = temp[-1]
return dim.upper()+ ' ' + ' '.join(temp[:-1]) + ' geometry'
else:
dim = self.quadric.ambient_dimension
sig = self.quadric.signature()
g = 'D Cayley-Klein geometry of signature '
return str(dim) + g + str(sig)
[docs]class EllipticGeometry(CayleyKleinGeometry):
"""Elliptic geometry class.
Parameters
----------
dimension : int
Dimension of the geometry.
is_dual : bool (default=False)
Whether geometry is dual or not.
Attributes
----------
dimension : int
Dimension of the geometry. This is the dimension of the model itself.
is_dual : bool
True if geometry is dual, else False
dual : EllipticGeometry
The dual geometry.
Methods
-------
set_alias
Set alias for the geometry.
inner_product
Inner product of the geometry, if it exists.
See Also
--------
Geometry, ProjectiveGeometry, CayleyKleinGeometry
"""
def __init__(self, dimension, is_dual=False):
super().__init__(self._inner_product, name='elliptic', is_dual=is_dual,
dimension=dimension)
@staticmethod
def _inner_product(v, w):
"""Self-dual elliptic inner product
"""
return np.dot(v, w)
[docs] def metric_to_cayley_klein_distance(self, d):
"""Return Metric distance converted to Cayley-Klein "distance".
K = cos(d) ** 2
Parameters
----------
d : float
Returns
-------
K : float
"""
return np.cos(d) ** 2
[docs] def cayley_klein_distance_to_metric(self, K):
"""Return Cayley-Klein distance converted to metric distance.
d is given by the equation ::
K = cos(d) ** 2
Parameters
----------
d : float
Returns
-------
K : float
"""
return np.arccos(np.sqrt(K))
[docs] def dualize(self):
prefix = 'dual ' if not self.is_dual else ''
return get_geometry(prefix + 'elliptic', dimension=self.dimension)
def __repr__(self):
return f'EllipticGeometry({self.dimension}, is_dual={self.is_dual})'
[docs]class EuclideanGeometry(CayleyKleinGeometry):
"""Euclidean geometry class.
Parameters
----------
dimension : int
Dimension of the geometry.
is_dual : bool (default=False)
Whether geometry is dual or not.
Attributes
----------
name : str
Name of the geometry.
dimension : int
Dimension of the geometry. This is the dimension of the model itself.
is_dual : bool
True if geometry is dual, else False
dual : EuclideanGeometry
The dual geometry.
Methods
-------
set_alias
Set alias for the geometry.
inner_product
Inner product of the geometry, if it exists.
See Also
--------
Geometry, ProjectiveGeometry, CayleyKleinGeometry
"""
def __init__(self, dimension, is_dual=False, affine_component=-1):
self.affine_component = affine_component
Q = inner_product.get_matrix(self._inner_product, dimension+1)
Q = quadrics.Quadric(Q)
if not is_dual:
Q = Q.dualize()
super().__init__(Q, name='euclidean', dimension=dimension,
is_dual=is_dual)
[docs] def dualize(self):
prefix = 'dual ' if not self.is_dual else ''
return get_geometry(prefix + 'euclidean', dimension=self.dimension)
[docs] def d(self, x, y):
"""Euclidean distance.
Dehomogenizes by `self.affine_component` and computes the norm of the
difference of `x` and `y`.
Note that the `affine_component` attribute of `Point` objects is
ignored.
Parameters
----------
x, y : Point or array_like of shape (dimension+1,)
Returns
-------
float
"""
i = self.affine_component
if isinstance(x, subspaces.Point):
x = x.point
x = np.array(x)
x = pmath.dehomogenize(x, i)
if isinstance(y, subspaces.Point):
y = y.point
y = np.array(y)
y = pmath.dehomogenize(y, i)
return np.linalg.norm(x - y)
@staticmethod
def _inner_product(v, w):
"""Dual euclidean inner product
"""
w = np.array(w)
w[-1] = 0
return np.dot(v,w)
def __repr__(self):
return f'EuclideanGeometry({self.dimension}, is_dual={self.is_dual})'
[docs]class HyperbolicGeometry(CayleyKleinGeometry):
"""Hyperbolic geometry class.
Parameters
----------
dimension : int
Dimension of the geometry.
is_dual : bool (default=False)
Whether geometry is dual or not.
Attributes
----------
name : str
Name of the geometry.
dimension : int
Dimension of the geometry. This is the dimension of the model itself.
is_dual : bool
True if geometry is dual, else False
dual : HyperbolicGeometry
The dual geometry.
Methods
-------
set_alias
Set alias for the geometry.
inner_product
Inner product of the geometry, if it exists.
See Also
--------
Geometry, ProjectiveGeometry, CayleyKleinGeometry
"""
def __init__(self, dimension, is_dual=False):
super().__init__(self._inner_product, name='hyperbolic',
is_dual=is_dual, dimension=dimension)
[docs] def metric_to_cayley_klein_distance(self, d):
"""Return Metric distance converted to Cayley-Klein "distance".
K = cosh(d) ** 2
Parameters
----------
d : float
Returns
-------
K : float
"""
return np.cosh(d) ** 2
[docs] def cayley_klein_distance_to_metric(self, K):
"""Return Cayley-Klein distance converted to metric distance.
d is given by the equation.
K = cosh(d) ** 2
Parameters
----------
d : float
Returns
-------
K : float
"""
return np.arccosh(np.sqrt(K))
[docs] def dualize(self):
prefix = 'dual ' if not self.is_dual else ''
return get_geometry(prefix + 'hyperbolic', dimension=self.dimension)
@staticmethod
def _inner_product(v, w):
"""Self-dual hyperbolic inner product
"""
w = np.array(w)
w[-1] = -w[-1]
return np.dot(v,w)
def __repr__(self):
return f'HyperbolicGeometry({self.dimension}, is_dual={self.is_dual})'
[docs]class LaguerreGeometry(CayleyKleinGeometry):
"""Laguerre geometry class.
Parameters
----------
dimension : int
Dimension of the geometry.
is_dual : bool (default=False)
Whether geometry is dual or not.
Attributes
----------
name : str
Name of the geometry.
dimension : int
Dimension of the geometry. This is the dimension of the model itself.
is_dual : bool
True if geometry is dual, else False
dual : LaguerreGeometry
The dual geometry.
Methods
-------
set_alias
Set alias for the geometry.
inner_product
Inner product of the geometry, if it exists.
See Also
--------
Geometry, ProjectiveGeometry, CayleyKleinGeometry
"""
dimension_offset = 1
def __init__(self, dimension, is_dual=False):
Q = inner_product.get_matrix(self._inner_product, dimension+1)
Q = quadrics.Quadric(Q)
if is_dual:
Q = Q.dualize()
super().__init__(Q, name='laguerre', dimension=dimension,
is_dual=is_dual)
[docs] def dualize(self):
prefix = 'dual ' if not self.is_dual else ''
return get_geometry(prefix + 'laguerre', dimension=self.dimension)
@staticmethod
def _inner_product(v, w):
"""Primal Laguerre-Blaschke inner product
"""
w = np.array(w)
w[-2] = 0
w[-1] = -w[-1]
return np.dot(v,w)
def __repr__(self):
return f'LaguerreGeometry({self.dimension}, is_dual={self.is_dual})'
[docs]class LieGeometry(CayleyKleinGeometry):
"""Elliptic geometry class.
Parameters
----------
dimension : int
Dimension of the geometry.
is_dual : bool (default=False)
Whether geometry is dual or not.
Attributes
----------
name : str
Name of the geometry.
dimension : int
Dimension of the geometry. This is the dimension of the model itself.
is_dual : bool
True if geometry is dual, else False
dual : LieGeometry
The dual geometry.
Methods
-------
set_alias
Set alias for the geometry.
inner_product
Inner product of the geometry, if it exists.
See Also
--------
Geometry, ProjectiveGeometry, CayleyKleinGeometry
"""
dimension_offset = 2
def __init__(self, dimension, is_dual=False):
super().__init__(self._inner_product, name='lie', is_dual=is_dual,
dimension=dimension)
@staticmethod
def _inner_product(v, w):
"""Self-dual Lie inner product
"""
w = np.array(w)
w[-2] = -w[-2]
w[-1] = -w[-1]
return np.dot(v,w)
[docs] def dualize(self):
prefix = 'dual ' if not self.is_dual else ''
return get_geometry(prefix + 'lie', dimension=self.dimension)
def __repr__(self):
return f'LieGeometry({self.dimension}, is_dual={self.is_dual})'
[docs]class MoebiusGeometry(CayleyKleinGeometry):
"""Moebius geometry class.
Parameters
----------
dimension : int
Dimension of the geometry.
is_dual : bool (default=False)
Whether geometry is dual or not.
Attributes
----------
name : str
Name of the geometry.
dimension : int
Dimension of the geometry. This is the dimension of the model itself.
is_dual : bool
True if geometry is dual, else False
dual : MoebiusGeometry
The dual geometry.
Methods
-------
set_alias
Set alias for the geometry.
inner_product
Inner product of the geometry, if it exists.
See Also
--------
Geometry, ProjectiveGeometry, CayleyKleinGeometry
"""
dimension_offset = 1
def __init__(self, dimension, is_dual=False):
super().__init__(self._inner_product, name='moebius', is_dual=is_dual,
dimension=dimension)
@staticmethod
def _inner_product(v,w):
"""Self-dual Möbius inner product
"""
w = np.array(w)
w[-1] = -w[-1]
return np.dot(v,w)
[docs] def dualize(self):
prefix = 'dual ' if not self.is_dual else ''
return get_geometry(prefix + 'moebius', dimension=self.dimension)
def __repr__(self):
return f'MoebiusGeometry({self.dimension}, is_dual={self.is_dual})'
[docs]def get_geometry(name, dimension=None):
"""Retrieve a geometry.
Geometries can only be retrieved, if they are named upon initialization, or
are one of the predefined geometries (elliptic, hyperbolic, laguerre,
moebius, euclicean and lie).
Parameters
----------
name : str
Name of the geometry
dimension : int (default=None)
Dimension of the geometry. This should be the model dimension.
Returns
-------
ddg.Geometry
Raises
------
KeyError
when the geometry could not be found nor created.
"""
if name is None:
raise KeyError(
'Geometries with name None can not be retrieved using '
'get_geometry.'
)
# If a geometry with this name and dimension already exists, return it.
if (name, dimension) in Geometry._geometries:
return Geometry._geometries[(name, dimension)]
# It didn't work. Either the geometry exists but we need to get the
# dimension parameter from the name or it doesn't exist and we have to
# create it.
is_dual = 'dual' in name.lower()
name_dim = re.findall(_DIM_REGEX, name)
name_dim = None if not name_dim else int(name_dim[0])
if dimension is None and name_dim is None:
raise KeyError(
"Dimension was neither given in name nor as argument."
)
for known_name, known_class in _KNOWN_GEOMETRIES.items():
if known_name not in name.lower():
continue
offset = known_class.dimension_offset
# Try again to return an existing geometry, this time with the
# dimension from the name with the appropriate offset
if dimension is None:
dimension = name_dim + offset
elif name_dim is not None and name_dim + offset != dimension:
raise ValueError(
f"Dimension given in name ({name_dim}) plus offset ({offset}) "
f"does not equal dimension given in parameter ({dimension})."
)
lookup_name = is_dual * 'dual ' + known_name
if (lookup_name, dimension) in Geometry._geometries:
return Geometry._geometries[(lookup_name, dimension)]
# Didn't work again, we have to create it.
geo = known_class(dimension=dimension, is_dual=is_dual)
# Set alias of the geometry so the user can retrieve it with the exact
# name with which they created it. Overwrite the actual name with the
# standard format, e.g. 'projective 3d'
geo.set_alias(f'{known_name} {geo.name_dimension}d', overwrite=True)
return geo
raise KeyError('Could not find nor create Geometry!')
def _remove_word_from_string(s, word):
"""Remove all instances of a word from a string.
An instance of (the sequence of characters of) the word appearing as part
of another word it will not be removed.
Parameters
----------
s : str
String to remove the word from.
word : str
Word to remove
Returns
-------
str
String with all instances of the word removed
"""
# Regex explanation
#
# (?P<start>(?<=\S)\s)? detects a word in front of the word to be removed.
# We have to remove a different amount of white space depending on it
#
# (?<!\s)\s*word(?!\S) detects instance of the word not contained in another
# one, e.g. 'test word' would match with 'word', but 'testword' wouldn't.
#
# (?(start)\s*(?=\s|$)|\s*) depending on whether the word was found at the
# beginning or in the middle of the string we will remove whitespaces until
# the next word (or the whitespace before it) is reached
regex = (
r'(?P<start>(?<=\S)\s)?(?<!\S)\s*'
+ word
+ r'(?!\S)(?(start)\s*(?=\s|$)|\s*)'
)
return re.sub(regex, '', s)
# Explanation of regex:
#
# (?i) - make matching case-insensitive
# (?<=\s)\d+(?=[\s-]?d) - match one or more digits preceded by a word boundary
# (whitespace, the start of a line or a non-alphanumeric symbol) and succeeded
# by either whitespace, '-' or nothing and then the letter 'd'.
_DIM_REGEX = r'(?i)(?<=\b)\d+(?=[\s-]?d)'
"""Regex to get dimension from geometry name such as 'projective 3d'.
Matches the formats ' 123d', ' 123-d' and ' 123 d', also with capital 'D'.
"""
_KNOWN_GEOMETRIES = {'projective': ProjectiveGeometry,
'elliptic': EllipticGeometry,
'hyperbolic': HyperbolicGeometry,
'laguerre': LaguerreGeometry,
'moebius': MoebiusGeometry,
'euclidean' : EuclideanGeometry,
'lie' : LieGeometry
}