# Tutorial: Sub-Riemannian geometry on the Heisenberg group#

 In [1]:

import geomstats.backend as gs
import geomstats

from geomstats.geometry.sub_riemannian_metric import SubRiemannianMetric
from geomstats.geometry.heisenberg import HeisenbergVectors

%matplotlib notebook

INFO: Using autograd backend


We import modules for plotting,

 In [2]:

import matplotlib.pyplot as plt
import matplotlib
from mpl_toolkits.mplot3d import Axes3D


## The Heisenberg group#

We instantiate the 3D heisenberg group,

 In [3]:

heis = HeisenbergVectors()


‘heis’ is an object representing a manifold, without any sub-Riemannian structure associated to it.

In geomstats, a sub-Riemannian structure is constructed via the SubRiemannianMetric-class. We now instantiate a SubRiemannianMetric corresponding to the heisenberg group.

In geomstats, a sub-Riemannian metric can be instantiated by supplying either

• a function which at an input point outputs the matrix representing the cometric, or

• a function which at an input point outputs a matrix whose columns are frame-vectors spanning the distribution at that point. This frame is then implicitly assumed to be orthonormal, so it determines both the distribution and the metric on the tangent bundle.

## Defining the Heisenberg sub-Riemannian structure via a frame-field#

We will construct the sub-Riemannian Heisenberg group by supplying a frame-function. Mathematically, we represent the distribution $$\Delta$$ in a standard way; at the point $$p \in heis$$ the distribution is

$\Delta_p = \text{span}(F_X(p), F_Y(p)),$

where $$F_X = L^{\star}_p(X)$$, $$F_Y = L^{\star}_p(Y)$$ and $$X = (1,0,0)$$, $$Y=(0,1,0)$$. So e.g. $$F_X$$ is the tangent left translation to point $$p$$ of the standard basis vector X in the Lie-algebra. In geomstats, this can be done concisely as follows,

 In [4]:

def heis_frame(point):
translations = heis.jacobian_translation(point)
return translations[..., 0:2]


Given the frame, we now instantiate the sub-Riemannian metric for the Heisenberg group

 In [5]:

heis_sr = SubRiemannianMetric(dim=3, frame=heis_frame)


We can illustrate the distribution by plotting the frame-vectors along a line:

 In [6]:

line = gs.array([k*gs.array([1., 1., 1.]) for k in gs.linspace(0,10,10)])
line = gs.array([k*gs.array([1., 0., 0.]) for k in gs.linspace(0,10,10)])

quiver_length = 0.1

fig1 = plt.figure()
ax  = plt.axes(projection='3d')

for i in range(line.shape[0]):
point = line[i,:]
frame_at_point = heis_sr.frame(point)

frame1 = frame_at_point[:, 1]
frame2 = frame_at_point[:, 0]

ax.quiver(point[0], point[1], point[2],
frame1[0], frame1[1], frame1[2],
length = quiver_length, normalize = True, color = 'orange',alpha=0.8,
linestyle = '-')
ax.quiver(point[0], point[1], point[2],
frame2[0], frame2[1], frame2[2],
length = quiver_length, normalize = True, color = 'blue',alpha=0.8,
linestyle = '-')


Everything is now set up to generate sub-Riemannian geodesics. A sub-Riemannian geodesic on a manifold $$M$$ is determined by an initial point $$p \in M$$ and an initial cotangent $$\lambda \in T_p M$$.

 In [7]:

base_point = gs.array([0., 0., 0.])
initial_cotangent = gs.array([1., 1., 1.])

times = gs.linspace(0., 10., 50)

path = heis_sr.geodesic(initial_point=base_point,
initial_cotangent_vec=initial_cotangent,
n_steps=1000)

path_1_1 = path(times)

 In [8]:

fig1 = plt.figure()
ax = plt.axes(projection='3d')
ax.scatter(0, 0, 0, marker='+', color='black')

ax.plot3D(path_1_1[:, 0],
path_1_1[:, 1],
path_1_1[:, 2],
'-',
linewidth=1.5,
markersize=0,
marker='.',
color='green',
alpha=0.7)

plt.show()


## Sub-Riemannian structures as limits of Riemannian structures#

If the distribution $$\Delta$$ is of the same dimension as the manifold at every point, the structure is actually Riemannian. In geomtats, if the manifold is $$n$$-dimensional and we pass a frame function with $$n$$ linearly independent vectors at every point, this will generate the Riemannian structure with a metric that makes these vectors orthonormal at every point.

For the heisenberg group, we can use the frame given by left-translation of the standard basis in the Lie algebra,

$\{F_X(p), F_Y(p), \epsilon\cdot F_Z(p)\},$

where $$\epsilon \in [0,1]$$. For $$\epsilon=0$$ the geodesics will be those of the standard Heisenberg group. For $$\epsilon = 1$$ the geodesics will be those of the left-invariant Riemannian metric which on the algebra is simply the euclidean metric.

 In [9]:

def heis_frame_riemannian(point, epsilon):
frame_matrix = heis.jacobian_translation(point)
scale_matrix = gs.array([gs.ones((3,)), gs.ones((3,)), gs.ones((3,))*epsilon]).T
return  gs.einsum('...ij,...ij -> ...ij', frame_matrix, scale_matrix)

heis_epsilon_1 = SubRiemannianMetric(dim=3, frame=lambda point : heis_frame_riemannian(point, 1))
heis_epsilon_0 = SubRiemannianMetric(dim=3, frame=lambda point : heis_frame_riemannian(point, 0))

 In [10]:

base_point = gs.array([0., 0., 0.])
initial_cotangent = gs.array([1., 1., 1.])

times = gs.linspace(0., 10., 50)

path_epsilon_1 = heis_epsilon_1.geodesic(initial_point=base_point,
initial_cotangent_vec=initial_cotangent,
n_steps=1000)

path_epsilon_0 = heis_epsilon_0.geodesic(initial_point=base_point,
initial_cotangent_vec=initial_cotangent,
n_steps=1000)

path_1 = path_epsilon_1(times)
path_0 = path_epsilon_0(times)

fig1 = plt.figure()
ax = plt.axes(projection='3d')
ax.scatter(0, 0, 0, marker='+', color='black')

ax.plot3D(path_1[:, 0],
path_1[:, 1],
path_1[:, 2],
'-',
linewidth=1.5,
markersize=0,
marker='.',
color='green',
alpha=0.7,
label='epsilon = 1')

ax.plot3D(path_0[:, 0],
path_0[:, 1],
path_0[:, 2],
'-',
linewidth=1.5,
markersize=0,
marker='.',
color='red',
alpha=0.7,
label='epsilon = 0')

ax.legend()

plt.show()


We see that the spiralling structure is similar for the Riemannian ($$\epsilon=1$$) and the sub-Riemannian ($$\epsilon=0$$) geodesics, in fact only their z-coordinates differ. Let’s check that the sub-Riemannian geodesic is horizontal wrt. to the distribution $$\Delta_p = \text{span}(F_X(p), F_Y(p))$$ (i.e. it’s tangent vectors lies in $$\Delta$$), whereas the Riemannian one isn’t.

 In [11]:

base_point = gs.array([0., 0., 0.])
initial_cotangent = gs.array([1., 1., 1.])

times = gs.linspace(0., 1., 50)

path_epsilon_1 = heis_epsilon_1.geodesic(initial_point=base_point,
initial_cotangent_vec=initial_cotangent)

path_epsilon_0 = heis_epsilon_0.geodesic(initial_point=base_point,
initial_cotangent_vec=initial_cotangent)

path_1 = path_epsilon_1(times)
path_0 = path_epsilon_0(times)

fig1 = plt.figure()
ax = plt.axes(projection='3d')
ax.scatter(0, 0, 0, marker='+', color='black')

ax.plot3D(path_1[:, 0],
path_1[:, 1],
path_1[:, 2],
'-',
linewidth=1.5,
markersize=0,
marker='.',
color='green',
alpha=0.7,
label='epsilon = 1')

ax.plot3D(path_0[:, 0],
path_0[:, 1],
path_0[:, 2],
'-',
linewidth=1.5,
markersize=0,
marker='.',
color='red',
alpha=0.7,
label='epsilon = 0')

ax.legend()

quiver_length = 1

exp_curve = path_1

for i in range(exp_curve.shape[0]):
if i % 10 == 0:
point = exp_curve[i,:]
frame_at_point = heis_sr.frame(point)

frame1 = frame_at_point[:, 1]
frame2 = frame_at_point[:, 0]

ax.quiver(point[0], point[1], point[2],
frame1[0], frame1[1], frame1[2],
length = quiver_length, normalize = True, color = 'orange',alpha=0.8,
linestyle = '-')
ax.quiver(point[0], point[1], point[2],
frame2[0], frame2[1], frame2[2],
length = quiver_length, normalize = True, color = 'blue',alpha=0.8,
linestyle = '-')

quiver_length = 1

exp_curve = path_0

for i in range(exp_curve.shape[0]):
if i % 10 == 0:

point = exp_curve[i,:]
frame_at_point = heis_sr.frame(point)

frame1 = frame_at_point[:, 1]
frame2 = frame_at_point[:, 0]

ax.quiver(point[0], point[1], point[2],
frame1[0], frame1[1], frame1[2],
length = quiver_length, normalize = True, color = 'pink',alpha=0.8,
linestyle = '-')
ax.quiver(point[0], point[1], point[2],
frame2[0], frame2[1], frame2[2],
length = quiver_length, normalize = True, color = 'yellow',alpha=0.8,
linestyle = '-')

ax.view_init(3, -96)

plt.show()