Source code for ddg.geometry.euclidean_models

"""Euclidean geometry module.

Contains model classes and functions for conversion between the models.
"""
import numpy as np

import ddg
import ddg.geometry.signatures as sg
import ddg.math.linalg as la
import ddg.math.projective as pmath
from ddg import nonexact
from ddg.geometry import laguerre_models, moebius_models
from ddg.geometry._intersection import intersect, join
from ddg.geometry._projections import north_pole
from ddg.geometry._quadrics import Quadric, intersect_quadric_subspace
from ddg.geometry._subspaces import (
    Point,
    coordinate_hyperplane,
    subspace_from_affine_points,
    subspace_from_affine_points_and_directions,
)
from ddg.geometry.geometry_model_templates import CayleyKleinGeometry, MetricGeometry
from ddg.geometry.spheres import QuadricSphere, _QuadricSphereGeometryBridge
from ddg.math.euclidean import angle_bisector_orientation_preserving as euc_abop
from ddg.math.euclidean import angle_bisector_orientation_reversing as euc_abor
from ddg.math.euclidean import reflection_in_a_hyperplane

__all__ = [
    "ProjectiveModel",
    "DualProjectiveModel",
    "MoebiusModel",
    "projective_to_moebius",
    "moebius_to_projective",
]


[docs]class DualProjectiveModel(CayleyKleinGeometry): """Dual projective model of Euclidean geometry. .. rubric:: Model space The model space is RP^n without [0,...,0, 1]. The points in this space can be thought of as hyperplanes in R^n. .. rubric:: Representation of objects Objects are just projective points, subspaces, quadrics and Cayley-Klein spheres that do not contain the origin. Parameters ---------- dimension : int Attributes ---------- dimension : int """ @property def absolute(self): """The absolute quadric with matrix ``diag([1,...,1, 0])``. Returns ------- Quadric """ matrix = np.eye(self.dimension + 1) matrix[-1, -1] = 0 return Quadric(matrix)
[docs] def angle(self, subspace1, subspace2): """Euclidean angle. Computes the angle between the duals of the subspaces. Parameters ---------- subspace1, subspace2 : ddg.geometry.Subspace Returns ------- float Raises ------ NotImplementedError If the subspaces have unsupported dimensions. """ if subspace1.dimension == 0 and subspace1.dimension == 0: arg = np.sqrt(self.cayley_klein_distance(subspace1, subspace2)) if ddg.nonexact.isclose(arg, 1): return 0.0 return np.arccos(arg) if subspace1.codimension == 1 and subspace2.codimension == 1: return ProjectiveModel(self.dimension).angle( subspace1.dual(), subspace2.dual() ) raise NotImplementedError( "Angle can currently only be computed between two vectors or " "between two hyperplanes." )
[docs] def laguerre(self): """Corresponding projective model (Blaschke cylinder) of Laguerre geometry.""" return laguerre_models.ProjectiveModel(self.dimension)
[docs] def to_laguerre(self, object_, embedded=False): raise NotImplementedError
[docs] def from_laguerre(self, object_, embedded=False): raise NotImplementedError
def __contains__(self, point): return self.inner_product(point, point) > 0 def __str__(self): return f"Dual projective model of {self.dimension}D Euclidean geometry"
[docs]class ProjectiveModel(CayleyKleinGeometry, MetricGeometry): """Euclidean geometry. .. rubric:: Model space The model space is RP^n without the points at infinity. .. rubric:: Representation of objects Objects are just projective points, subspaces and quadrics not at infinity and Euclidean spheres. Parameters ---------- dimension : int Attributes ---------- dimension : int """ @property def absolute(self): """The absolute quadric. This is the dual quadric of the quadric with matrix ``diag([1,...,1, 0])``. As the dual of a degenerate quadric, it is contained in the hyperplane at infinity. Returns ------- ddg.geometry.Quadric """ return DualProjectiveModel(self.dimension).absolute.dual()
[docs] @staticmethod def d(x, y): """Euclidean distance. Dehomogenizes and computes the norm of the difference of `x` and `y`. Parameters ---------- x, y : Point or array_like of shape (dimension+1,) Returns ------- float """ if not isinstance(x, Point): x = Point(x) if not isinstance(y, Point): y = Point(y) return np.linalg.norm(x.affine_point - y.affine_point)
[docs] def angle(self, subspace1, subspace2): """Euclidean angle. If both subspaces are points, the angle between them as vectors will be returned, up to pi. Otherwise, the smaller angle between the subspaces will be returned, up to pi/2. Currently only hyperplanes are supported in the latter case. Parameters ---------- subspace1, subspace2 : ddg.geometry.Subspace Returns ------- float Raises ------ NotImplementedError If the subspaces have unsupported dimensions. """ if subspace1.dimension == 0 and subspace2.dimension == 0: p1 = subspace1.affine_point p2 = subspace2.affine_point p1 /= np.linalg.norm(p1) p2 /= np.linalg.norm(p2) inn_prod = np.dot(p1, p2) if ddg.nonexact.isclose(inn_prod, 1): return 0.0 if ddg.nonexact.isclose(inn_prod, -1): return np.pi return np.arccos(inn_prod) if subspace1.codimension == 1 and subspace2.codimension == 1: return DualProjectiveModel(self.dimension).angle( subspace1.dual(), subspace2.dual() ) raise NotImplementedError( "Angle can currently only be computed between two vectors or " "between two hyperplanes." )
[docs] def moebius(self): """Corresponding projective model of Moebius geometry.""" return MoebiusModel(self.dimension)
[docs] def to_moebius(self, object_, embedded=False): """Alias for :py:func:`.projective_to_moebius`""" return projective_to_moebius(object_, embedded=embedded)
[docs] def from_moebius(self, object_, embedded=False): """Alias for :py:func:`.moebius_to_projective`""" return moebius_to_projective(object_, embedded=embedded)
[docs] def laguerre(self): """Corresponding projective model (Blaschke cylinder) of Laguerre geometry.""" return laguerre_models.ProjectiveModel(self.dimension)
[docs] def to_laguerre(self, object_): """Alias for :py:func:`.laguerre_models.euclidean_to_projective`""" return laguerre_models.euclidean_to_projective(object_)
[docs] def from_laguerre(self, object_): """Alias for :py:func:`.laguerre_models.projective_to_euclidean`""" return laguerre_models.projective_to_euclidean(object_)
[docs] @staticmethod def angle_bisectors(hyperplane1, hyperplane2): """Create the two angle bisecting hyperplanes of two given hyperplanes. The first entry in the output is the angle bisector that is orientation preserving. The second one is orientation reversing. Parameters ---------- hyperplane1 : ddg.geometry.Subspace hyperplane2 : ddg.geometry.Subspace Returns ------- (ddg.geometry.Subspace, ddg.geometry.Subspace) See Also -------- ddg.geometry.angle_bisector_orientation_preserving ddg.geometry.angle_bisector_orientation_reversing """ pre = ProjectiveModel.angle_bisector_orientation_preserving( hyperplane1, hyperplane2 ) rev = ProjectiveModel.angle_bisector_orientation_reversing( hyperplane1, hyperplane2 ) return (pre, rev)
[docs] @staticmethod def angle_bisector_orientation_preserving(hyperplane1, hyperplane2): """Create the orientation preserving angle bisecting hyperplane of two given hyperplanes. It is orientation preserving in the sense, that reflection in this angle bisector maps one hyperplane to the other with matching orientation. Parameters ---------- hyperplane1 : ddg.geometry.Subspace hyperplane2 : ddg.geometry.Subspace Returns ------- ddg.geometry.Subspace See Also -------- ddg.math.euclidean.angle_bisector_orientation_preserving """ n1, d1 = (ddg.geometry.normal(hyperplane1), ddg.geometry.level(hyperplane1)) n2, d2 = (ddg.geometry.normal(hyperplane2), ddg.geometry.level(hyperplane2)) n_pre, d_pre = euc_abop(n1, d1, n2, d2) return ddg.geometry.hyperplane_from_normal(n_pre, level=d_pre)
[docs] @staticmethod def angle_bisector_orientation_reversing(hyperplane1, hyperplane2): """Create the orientation reversing angle bisecting hyperplane of two given hyperplanes. It is orientation reversing in the sense, that reflection in this angle bisector maps one hyperplane to the other with opposite orientation. Parameters ---------- hyperplane1 : ddg.geometry.Subspace hyperplane2 : ddg.geometry.Subspace Returns ------- ddg.geometry.Subspace See Also -------- ddg.math.euclidean.angle_bisector_orientation_reversing """ n1, d1 = (ddg.geometry.normal(hyperplane1), ddg.geometry.level(hyperplane1)) n2, d2 = (ddg.geometry.normal(hyperplane2), ddg.geometry.level(hyperplane2)) n_rev, d_rev = euc_abor(n1, d1, n2, d2) return ddg.geometry.hyperplane_from_normal(n_rev, level=d_rev)
[docs] @staticmethod def perpendicular_bisector(p1, p2): """Create a hyperplane orthogonal to the join of p1 and p2. It intersects this line in the midpoint of p1.affine_point and p2.affine_point. Parameters ---------- p1: ddg.geometry.Point p2: ddg.geometry.Point Returns ------- ddg.geometry.Subspace Hyperplane that is orthogonal to the input line. """ p1_affine = np.array(p1.affine_point) p2_affine = np.array(p2.affine_point) point = (p1_affine + p2_affine) / 2 direction = p1_affine - p2_affine # We get a direction of our line and use that # as the normal for the newly created hyperplane. bisector_hyperplane = ddg.geometry.hyperplane_from_normal( direction, point=point ) return bisector_hyperplane
[docs] @staticmethod def reflect_in_hyperplane(subspace, hyperplane): """Reflects a subspace in a hyperplane. Parameters ---------- subspace : ddg.geometry.Subspace Subspace to reflect in the hyperplane. hyperplane : ddg.geometry.Subspace Hyperplane in n-dimensional ambient space. Raises ------ ValueError if the reflecor is not a hyperplane, if the hyperplane is at infinity, or if the ambient dimensions don't match. Returns ------- reflected_hyperplane : ddg.geometry.Subspace The reflected subspace. """ if hyperplane.codimension != 1: raise ValueError( "Reflector must be a hyperplane, " f"but is of dimension {hyperplane.dimension}" f"in {hyperplane.ambient_dimension}-dimensional space." ) if hyperplane.at_infinity(): raise ValueError("Hyperplane is at infinity.") if hyperplane.ambient_dimension != subspace.ambient_dimension: raise ValueError( "Subspace and hyperplane must have the same ambient dimension. " f"The subspace has dimension {subspace.ambient_dimension}. " f"The hyperplane has dimension {hyperplane.ambient_dimension}" ) Nl = ddg.geometry.normal_with_level(hyperplane) normal = Nl[:-1] level = -Nl[-1] reflection_matrix = reflection_in_a_hyperplane(normal, level) return ddg.geometry.Subspace(*(reflection_matrix @ subspace.matrix).T)
[docs] def ellipse_from_foci(self, f1, f2, a): """Get ellipse in 2D from foci and distance parameter. The returned ellipse will be the set:: {x : |x - f1| + |x - f2| = 2a} Parameters ---------- f1, f2 : Point a : float 2 * a must ge greater than the distance between the foci. Returns ------- ddg.geometry.Quadric Raises ------ ValueError If 2 * a is not greater than the distance between the foci. """ if self.dimension != 2: raise NotImplementedError("Function ellipse_from_foci only works in 2D.") f1 = f1.affine_point f2 = f2.affine_point c = np.linalg.norm(f1 - f2) / 2 if a <= c: raise ValueError( "2 * a must be greater than the distance between the foci." ) # We first create the standard ellipse centered at the origin with the # x-axis being the major axis. Then we rotate and translate it # appropriately. Q = Quadric(np.diag([1 / (a**2), 1 / (a**2 - c**2), -1])) # It doesn't matter which way the difference goes because of symmetries of the # ellipse. angle = np.angle(complex(*(f2 - f1))) A = ddg.math.euclidean2d.rotation(angle) F = pmath.affine_transformation(A, (f1 + f2) / 2) Q = Q.transform(F) return Q
[docs] @classmethod def sphere(cls, center, radius, subspace=None, atol=None, rtol=None): if not isinstance(center, Point): center = Point(center) dim = center.ambient_dimension geometry_bridge = _EuclideanQSGB(cls(dim)) return QuadricSphere( center, radius, subspace=subspace, geometry_bridge=geometry_bridge, atol=atol, rtol=rtol, )
[docs] def sphere_through_points(self, *points): """Find the unique k-sphere through given points. The sphere might degenerate to subspace. Generically, n points define an (n-2)-sphere. If n given points are contained in a lower-dimensional sphere, this sphere will be returned. Parameters ---------- *points : iterable with types `ddg.geometry.Point` or `float` The points for which to find the sphere. The point at infinity can be passed as the floating point representation of positive infinity, e.g., as `numpy.inf`. Returns ------- SphereLike or Subspace Raises ------ ValueError - If the points are not contained in a sphere. - If a given point is not an element of the according geometry. - If only the point at infinity is given. See also -------- ddg.geometry.hyperbolic_models.ProjectiveModel.orthogonal_sphere, ddg.geometry.elliptic_models.ProjectiveModel.orthogonal_sphere """ mob = MoebiusModel(self.dimension) # Lift the points to Möbius geometry lifted_points = [] for point in points: if point == np.inf: lifted_points.append(north_pole(self.dimension + 1)) elif point in self: lifted_points.append(projective_to_moebius(point)) else: raise ValueError(f"A given point is not an element of {self}.") # Create the subspace to get the plane of the sphere plane = join(*lifted_points) if plane.dimension > self.dimension: # The plane consists of the whole space. # Thus, there is no sphere through the given points raise ValueError("There is no sphere containing the given points.") if plane == north_pole(self.dimension + 1): raise ValueError( "Only the point at infinity was given which does " "not define a Euclidean sphere." ) # Intersect the plane with the Möbius quadric and project back lifted_sphere = intersect(plane, mob.absolute) return moebius_to_projective(lifted_sphere)
[docs] @classmethod def sphere_from_affine_point_and_normals( cls, point, radius, normals=None, atol=None, rtol=None ): """Create a Euclidean sphere from an affine point and normal directions. Parameters ---------- point : array_like of shape (n,) Affine coordinate vector in n-dimensional space. radius : float normals : array_like of shape (k, n) (default=None) The rows/elements of `normals` are vectors in affine coordinates. `sphere.subspace` will be the affine subspace :: orthogonal_complement(span(normals)) + point. The default is an empty array, i.e. `sphere.subspace` will be the whole space. atol, rtol : float (default=None) Tolerances for the sphere. Returns ------- sphere : ddg.geometry.spheres.SphereLike Let k'=dim(span(normals)). Returns a Euclidean (n-k'-1)-sphere contained in the (n-k')-dimensional projective subspace described above. """ center = subspace_from_affine_points(point) if normals is None: normals = np.array([]) else: normals = np.array(normals) if normals.size == 0: return cls.sphere(center, radius, atol=atol, rtol=rtol) directions = list(la.nullspace(normals).T) subspace = subspace_from_affine_points_and_directions(point, directions) return cls.sphere(center, radius, subspace=subspace, atol=atol, rtol=rtol)
[docs] def orthogonal_sphere(self, *spheres): """Find the intersection of all spheres orthogonal to the given spheres. If the output of this function is a non-degenerate sphere (i.e., sphere with positive radius), then this sphere possesses the following property: Any higher-dimensional sphere containing the former sphere is orthogonal to all spheres given in the input. Thus, it is the orthogonal sphere of minimal dimension, if it is unique. For example, m non-intersecting hyperspheres in general position in n-dimensional Euclidean space have a unique orthogonal (m-2)-sphere, which is returned by this function. Subspaces can also be inserted or returned. Returns None if the intersection is empty (including the case when there is no orthogonal spheres). Parameters ---------- *spheres: iterable of type `ddg.geometry.spheres.SphereLike` or `ddg.geometry.Subspace` Returns ------- `ddg.geometry.spheres.SphereLike` or `ddg.geometry.Subspace` or `None` See also -------- ddg.geometry.hyperbolic_models.ProjectiveModel.orthogonal_sphere, ddg.geometry.elliptic_models.ProjectiveModel.orthogonal_sphere """ mob = MoebiusModel(self.dimension) # Lift the spheres to Moebius model mob_spheres = [projective_to_moebius(sph) for sph in spheres] # Find the intersection of their subspaces mob_subspaces = [sph.subspace for sph in mob_spheres] polar_subspace = intersect(*mob_subspaces) if polar_subspace.dimension < 0: # There is no sphere orthogonal to the given spheres return None # Take the sphere in Moebius model polar to the intersection ort_sphere_subspace = mob.absolute.polarize(polar_subspace) ort_sphere_mob = intersect(mob.absolute, ort_sphere_subspace) if ort_sphere_mob.dimension < 0: # The intersection of the set of orthogonal spheres is empty. return None # Project the sphere back to Euclidean model ort_sphere_euc = moebius_to_projective(ort_sphere_mob) return ort_sphere_euc
[docs] @staticmethod def radical_subspace(*spheres): """Find the subspace spanned by the centers of all hyperspheres orthogonal to given spheres, or the radical center of given spheres. For two circles in Euclidean plane the result is usually called the radical line. More generally, for two hyperspheres in Euclidean space the returned subspace is called the radical hyperplane. Consider two spheres. * If the spheres do not have common points, the radical subspace is the subspace consisting of the centers of all hyperspheres orthogonal to two given spheres. * If the spheres touch, the radical subspace is the common touching subspace. * If the spheres intersect, the radical subspace the subspace spanned by their common sphere. The radical subspace of more than two spheres is the intersection of their pairs' radical subspaces. A point on the radical subspace outside all given spheres represents the center of a common orthogonal Euclidean hypersphere. If the dimension of the radical subspace is non-zero, then such hyperspheres exist. If the dimension is zero, then the point is called the radical center. Parameters ---------- *spheres: iterable of type `ddg.geometry.spheres.SphereLike` or `ddg.geometry.Subspace` Returns ------- `ddg.geometry.Subspace` or `None` """ # Lift the spheres to Moebius model mob_spheres = [projective_to_moebius(sph) for sph in spheres] # Find the intersection of their subspaces mob_subspaces = [sph.subspace for sph in mob_spheres] common_subspace = intersect(*mob_subspaces) if common_subspace.dimension < 0: # There is no sphere orthogonal to the given spheres return None # All the points in `common_subspace` are polar to the hyperspheres # orthogonal to `mob_spheres`. The points are mapped to centers of # corresponding Euclidean hyperspheres under stereographic projection. return moebius_to_projective(common_subspace)
[docs] @staticmethod def sphere_to_quadric(sphere): """Convert Euclidean sphere to quadric Parameters ---------- sphere : ddg.geometry.spheres.SphereLike Returns ------- ddg.geometry.Quadric """ # This is necessary because quadric.transform actually transforms # the quadric's subspace and can only handle invertible matrices. This # should be fixed once transform returns a new object instead of # mutating. atol = sphere.atol rtol = sphere.rtol if nonexact.isclose(sphere.radius, 0, atol=atol, rtol=rtol): return Quadric(np.array([[0]]), subspace=sphere.center) r = sphere.radius c = sphere.center.affine_point k = sphere.ambient_dimension Q = np.eye(k + 1) Q[-1, -1] = -1 Q = Quadric(Q) F = pmath.affine_transformation(r * np.eye(k), c) Q = Q.transform(F) if sphere.subspace.codimension > 0: Q = intersect_quadric_subspace(Q, sphere.subspace) Q.atol = atol Q.rtol = rtol return Q
[docs] @classmethod def quadric_to_sphere(cls, quadric, atol=None, rtol=None): """Convert quadric to a Euclidean sphere, if it is one. Parameters ---------- quadric : ddg.geometry.Quadric atol, rtol : float (default=None) If None is given, the global defaults are used. See ddg.nonexact for details. Returns ------- ddg.geometry.Sphere Raises ------ ValueError If quadric is not a sphere. """ # Check signature sgn = quadric.signature(affine=True) k = quadric.subspace.dimension is_point = sgn == sg.AffineSignature(k, 0, 1, last_entry=0) is_regular_sphere = sgn == sg.AffineSignature(k, 1, last_entry=-1) if not (is_point or is_regular_sphere): raise ValueError( f"Signature of quadric is {sgn}, but has to be one of:\n" "- (k, 1) with -1 in the last position to be a regular sphere,\n" "- or (k, 0, 1) with 0 in the last position to be a point." ) Q = quadric.normalize(affine=True) B = Q.subspace.matrix try: r, _, c = pmath.decompose_similarity(B, atol=atol, rtol=rtol) except ValueError as exc: raise ValueError("Quadric is not a Euclidean sphere.") from exc c = subspace_from_affine_points(c) if is_point: r = 0 return cls.sphere(c, r, subspace=Q.subspace)
def __contains__(self, point): return not point.at_infinity() def __str__(self): return f"Projective model of {self.dimension}D Euclidean geometry"
class _EuclideanQSGB(_QuadricSphereGeometryBridge): """Auxiliary object for spheres. See the spheres developer's guide for details.""" def __init__(self, geometry): self.geometry = geometry def validate_sphere(self, sphere): if sphere.center.at_infinity(): raise ValueError("Center of Euclidean spheres can not be at infinity.") @property def model_dimension(self): return self.geometry.dimension def sphere_to_quadric(self, sphere): return self.geometry.sphere_to_quadric(sphere) def embed(self, sphere, subspace=None): if subspace is None: subspace = coordinate_hyperplane(sphere.ambient_dimension + 1) try: # The last basis vector must be a point not at infinity and the others # must be a scaled orthonormal basis of the subspace. Determine the # scaling s. s, _, _ = pmath.decompose_similarity( subspace.matrix, atol=sphere.atol, rtol=sphere.rtol ) except ValueError as exc: raise ValueError( "This embedding would not result in a Euclidean sphere. If " "this is intentional, convert the sphere to a Quadric before " "embedding." ) from exc return self.geometry.sphere( sphere.center.embed(subspace), s * sphere.radius, sphere.subspace.embed(subspace), ) def unembed(self, sphere, subspace=None): if subspace is None: subspace = coordinate_hyperplane(sphere.ambient_dimension) if not sphere.subspace <= subspace: raise ValueError("Given subspace must contain sphere.") try: # The last basis vector must be a point not at infinity and the others # must be a scaled orthonormal basis of the subspace. Determine the # scaling s. s, _, _ = pmath.decompose_similarity( subspace.matrix, atol=sphere.atol, rtol=sphere.rtol ) except ValueError as exc: raise ValueError( "This coordinate computation would not result in a Euclidean " "sphere. If this is intentional, convert the sphere to a " " Quadric and compute its coordinates." ) from exc return self.geometry.sphere( sphere.center.unembed(subspace), sphere.radius / s, sphere.subspace.unembed(subspace), ) def __repr__(self): return "_EuclideanQSGB(" + repr(self.geometry) + ")"
[docs]class MoebiusModel(moebius_models._ProjectiveSubgeometry): """Euclidean geometry as a subgeometry of Möbius geometry. .. rubric:: Model space The model space is the standard Möbius quadric with matrix `diag([1,...,1, -1])` without `fixed_point`. .. rubric:: Representation of objects Subspaces and spheres are represented as Möbius spheres, i.e. Quadric objects contained in the Möbius quadric. Parameters ---------- dimension : int Attributes ---------- dimension : int Notes ----- This class supports comparison with `==`. Two geometries are equal if and only if they have the same type and `dimension`. This class also supports the `in` operator, which checks whether a point is in the model space of this geometry, i.e. the Möbius quadric. """ @property def fixed_point(self): """Projection point on the Möbius quadric. Used to compute metric. This is the north pole with representative [0,...,0, 0.5, 0.5]. Returns ------- ddg.geometry.Point """ return self.euclidean_point
[docs] def d(self, v, w): """Euclidean distance in the Möbius model. Given by :: d([x], [y])^2 = (-1/2) * <x, y> / (<x, p>, <y, p>) where `p` is `self.fixed_point` and `x` and `y` are points in the Möbius quadric not equal to `p`. Parameters ---------- x, y : ddg.geometry.Point or array_like of shape (dimension+2,) Returns ------- float """ if isinstance(v, Point): v = v.point if isinstance(w, Point): w = w.point p = self.fixed_point.point b = self.inner_product return np.sqrt(-0.5 * b(v, w) / (b(v, p) * b(w, p)))
def __contains__(self, point): return point in self.absolute and point != self.fixed_point def __str__(self): return ( f"Spherical model of {self.dimension}D Euclidean geometry\n" + f"in the projective model of {self.dimension}D Moebius geometry" )
[docs]def projective_to_moebius(object_, embedded=False): """Alias for :py:func:`.moebius_models.euclidean_to_projective`""" return moebius_models.euclidean_to_projective(object_, embedded=embedded)
[docs]def moebius_to_projective(object_, embedded=False): """Alias for :py:func:`.moebius_models.projective_to_euclidean`""" return moebius_models.projective_to_euclidean(object_, embedded=embedded)