Subspaces
The ddg.geometry module contains classes and functions related to projective subspaces, i.e. points, lines, planes etc. These are instances of Subspace or Point.
Creating subspaces
The Subspace class is initialized with homogeneous coordinates:
>>> import ddg
>>> subspace = ddg.geometry.Subspace([1, 0, 1, 0], [0, 1, 1, 1])
>>> subspace
Subspace(array([1., 0., 1., 0.]), array([0., 1., 1., 1.]))
>>> print(subspace)
Line in 3D projective space
Homogeneous coordinates (columns):
[[1. 0.]
[0. 1.]
[1. 1.]
[0. 1.]]
>>> subspace.dimension
1
>>> subspace.ambient_dimension
3
>>> subspace.at_infinity()
False
To get the points in affine coordinates, use the methods Subspace.affine_points() and Subspace.affine_matrix().
Note that we have passed the points as separate arguments. To create a subspace
using the columns or rows of a matrix as points, we have the utility functions
subspace_from_columns() and
subspace_from_rows():
>>> import numpy as np
>>> points = np.array([[1, 0], [0, 1], [1, 1], [0, 1]])
>>> subspace = ddg.geometry.subspace_from_columns(points)
>>> subspace
Subspace(array([1., 0., 1., 0.]), array([0., 1., 1., 1.]))
There is also subspace_from_affine_points():
>>> x_axis = ddg.geometry.subspace_from_affine_points([0, 0, 0], [1, 0, 0])
>>> print(x_axis)
Line in 3D projective space
Homogeneous coordinates (columns):
[[0. 1.]
[0. 0.]
[0. 0.]
[1. 1.]]
There is a special subclass Point
that subspaces with a single point automatically get cast to:
>>> p = ddg.geometry.Subspace([1, 0, 1, 0])
>>> p
Point(array([1., 0., 1., 0.]))
>>> print(p)
Point in 3D projective space
Homogeneous coordinates: [1. 0. 1. 0.]
>>> p.point
array([1., 0., 1., 0.])
>>> p.at_infinity()
True
It is also possible to create a \(k\)-dimensional least-square subspace using least_square_subspace_from_affine_points() given the affine coordinates of \(N\) points.
>>> affine_coordinates = [
... np.array([-5.2, 3.1, 3.0, 4.6]),
... np.array([1.2, 0.2, 5.0, 1.0]),
... np.array([1.56, 0.9, 2.1, 0.43]),
... np.array([6.4, 4.0, 6.0, 0.0]),
... ]
>>> line = ddg.geometry.least_square_subspace_from_affine_points(
... affine_coordinates, k=1
... )
>>> print(line)
Line in 4D projective space
Homogeneous coordinates (columns):
[[ 0.99 0.90091871]
[ 2.05 0.0336353 ]
[ 4.025 0.22091693]
[ 1.5075 -0.37203476]
[ 1. 0. ]]
>>> plane = ddg.geometry.least_square_subspace_from_affine_points(
... affine_coordinates, k=2
... )
>>> print(plane)
Plane in 4D projective space
Homogeneous coordinates (columns):
[[ 0.99 0.90091871 -0.01438605]
[ 2.05 0.0336353 -0.827788 ]
[ 4.025 0.22091693 -0.4272281 ]
[ 1.5075 -0.37203476 -0.36336788]
[ 1. 0. 0. ]]
Note that passing k=-1 returns a least-square hyperplane, i.e a subspace of dimension one less than ambient dimension.
>>> hyperplane = ddg.geometry.least_square_subspace_from_affine_points(
... affine_coordinates, k=-1
... )
>>> print(hyperplane)
Hyperplane in 4D projective space
Homogeneous coordinates (columns):
[[ 0.99 0.90091871 -0.01438605 0.15173223]
[ 2.05 0.0336353 -0.827788 0.4826233 ]
[ 4.025 0.22091693 -0.4272281 -0.85701824]
[ 1.5075 -0.37203476 -0.36336788 -0.09783564]
[ 1. 0. 0. 0. ]]
>>> hyperplane.dimension
3
>>> hyperplane.ambient_dimension
4
We can also pass a list of points as Point directly to the function least_square_subspace() where their affine coordinates are automatically used to get a \(k\)-dimensional least-square subspace.
>>> points = [
... ddg.geometry.Point(np.array([1.0, 2.0, 1.0, 1.0])),
... ddg.geometry.Point(np.array([0.0, 0.0, 1.0, 1.0])),
... ddg.geometry.Point(np.array([2.0, 3.0, 3.0, 1.0])),
... ddg.geometry.Point(np.array([5.0, 7.0, 1.0, 1.0])),
... ddg.geometry.Point(np.array([6.0, 8.1, 0.0, 1.0])),
... ]
>>> plane = ddg.geometry.least_square_subspace(points, 2)
>>> print(plane)
Plane in 3D projective space
Homogeneous coordinates (columns):
[[ 2.8 0.59954182 -0.03854255]
[ 4.02 0.79259899 -0.11002335]
[ 1.2 -0.1110696 -0.99318142]
[ 1. 0. 0. ]]
>>> plane.dimension
2
>>> plane.ambient_dimension
3
Finally, we can use whole_space() to get the entire projective space of a specific dimension.
>>> whole_three_space = ddg.geometry.whole_space(3)
>>> print(whole_three_space)
Whole space in 3D projective space
Homogeneous coordinates (columns):
[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]
Intersecting and joining
We can intersect and join subspaces with the functions ddg.geometry.meet and ddg.geometry.join. The join is similar to the sum of vector spaces.
>>> line = ddg.geometry.subspace_from_affine_points([0, 1, -1], [1, 0, 0])
>>> point = ddg.geometry.subspace_from_affine_points([0, 0, 1.5])
>>> plane = ddg.geometry.subspace_from_affine_points(
... [1, 0, 0], [2, 1, 0], [1, 0, 1]
... )
>>> join_plane = ddg.geometry.join(line, point)
>>> meet_line = ddg.geometry.meet(join_plane, plane)
>>> meet_point = ddg.geometry.meet(line, meet_line)
We can see all of these subspaces in the image below. The explicitly created objects are shown in blue, the join of the line and the point in yellow. The intersection of the plane and the line is shown in light green and the intersection of the plane and the join in dark green.
Dualization
To every k-dimensional subspace in n-dimensional projective space corresponds an (n-k-1)-dimensional dual subspace. In the simplest case, it essentially arises from taking the orthogonal complement of the subspace basis as the basis for the dual subspace.
>>> p_dual = p.dual()
>>> p.dimension
0
>>> p_dual.dimension
2
Converting subspaces to nets
Subspaces of any dimension can simply be converted to nets using ddg.conversion.nets.core.to_smooth_net(), accessible directly as ddg.to_smooth_net. There are two different parametrizations available using the boolean keyword argument convex. You can also choose whether the net should output homogeneous or affine coordinates using the keyword affine. Both are True by default since that yields a net of the kind needed for
The “convex” parametrization
The convex=True parametrization is the one used for It only works for subspaces not at infinity and depends on the given basis of the subspace, i.e. subspace.points, which we will write as \((u_0,\dots,u_k)\). We denote by \(\tilde{u}_i\) the \(i\)-th basis vector dehomogenized, i.e. with last component normalized to either 0 or 1. First, we take a look at the two most important special cases:
The first is where all basis vectors \(u_0,\dots,u_k\) are points not at infinity. the parametrization is
\[[\tilde{u}_0 + \lambda_1 (\tilde{u}_1 - \tilde{u}_0) + \dots + \lambda_k (\tilde{u}_k - \tilde{u}_0)],\]so in particular \((0,\dots,0) \mapsto [u_0]\) and \(e_j \mapsto [u_j]\). This form is useful for drawing line segments for example: Say you are converting
ddg.geometry.Subspace(u0, u1)to a curve. Simply restrict the domain of the curve to[0, 1]and you will get the line segment between \([u_0]\) and \([u_1]\) (See Restricting the domain).In the 2D case, this will produce a parallelogram.
The other special case is where one basis vector (assume \(u_0\)) is a point not at infinity and all others are at infinity. In this case, the parametrization is
\[[\tilde{u}_0 + \lambda_1 u_1 + \dots + \lambda_k u_k].\]This case is useful for getting parametrizations that are centered around a certain point. For example, if the \(u_1,\dots,u_k\) are orthonormal and you restrict the domain to
[[-1, 1],...,[-1, 1]], you will get a symmetric parametrization around \([u_0]\), for example a square or a cube.
Now the general case: Let \((u_{ij})\) be subspace.matrix, i. e. \(u_{ij}\) is the \(i\)-th entry of the basis vector \(u_j\). We assume that \(u_0\) is not at infinity. If it is, a vector not at infinity will be moved to the first position. Then the full formula is:
This parametrization always produces vectors with last entry equal to 1.
>>> subspace = ddg.geometry.Subspace([0, 0, 1, 0]).dual()
>>> print(subspace)
Plane in 3D projective space
Homogeneous coordinates (columns):
[[0. 1. 0.]
[1. 0. 0.]
[0. 0. 0.]
[0. 0. 1.]]
>>> plane_net = ddg.to_smooth_net(subspace)
>>> plane_net(0, 0)
array([0., 0., 0.])
>>> plane_net(1, 0)
array([0., 1., 0.])
>>> plane_net(0, 1)
array([1., 0., 0.])
There are a few utility functions and methods available that help with creating better “convex” parametrizations:
ddg.geometry.orthonormalize_subspace()aliasSubspace.orthonormalize()Computes a completely new basis \((u_0,\dots,u_k)\) that produces a parametrization of the form \(u_0 + \lambda_1 u_1 + \dots + \lambda_k u_k\), where \(u_0\) is the point in the subspace closest to the origin and \(u_1,\dots,u_k\) are orthonormal direction vectors at infinity.
ddg.geometry.center_subspace()aliasSubspace.center().This sets the center of the parametrization. Basically replaces \(u_0\) with any other desired point in the subspace.
ddg.geometry.orthonormalize_and_center_subspace()aliasSubspace.orthonormalize_and_center().Sets the center of the parametrization and orthonormalizes the basis simultaneously.
The homogeneous parametrization
The convex=False parametrization takes k+1 parameters and simply returns a
linear combination of the given basis vectors.
>>> subspace = ddg.geometry.Subspace([0, 0, 1, 0]).dual()
>>> print(subspace)
Plane in 3D projective space
Homogeneous coordinates (columns):
[[0. 1. 0.]
[1. 0. 0.]
[0. 0. 0.]
[0. 0. 1.]]
>>> plane_net_homogeneous = ddg.to_smooth_net(subspace, affine=False, convex=False)
>>> plane_net_homogeneous(1, 0, 0)
array([0., 1., 0., 0.])
>>> plane_net_homogeneous(1, 1, 1)
array([1., 1., 0., 1.])
Restricting the domain
To restrict the domain, to_smooth_net has a keyword domain. It can take
either a domain object or its representation as a list of lists. See
Domains for more information.
>>> plane_net = ddg.to_smooth_net(subspace, convex=True, affine=True)
>>> plane_net.domain
SmoothRectangularDomain([[-inf, inf, False], [-inf, inf, False]])
>>> plane_net_restricted = ddg.to_smooth_net(
... subspace, convex=True, affine=True, domain=[[0, 1], [0, 1]]
... )
>>> plane_net_restricted.domain
SmoothRectangularDomain([[0.0, 1.0, False], [0.0, 1.0, False]])
Visualization in Blender
A guide on how to visualize Subspaces in Blender can be found here: Visualizing with blender.

