Source code for ddg.geometry.geometries

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 }