Affine Connections

The class AffineConnection implements affine connections on smooth manifolds.

AUTHORS:

  • Eric Gourgoulhon, Michal Bejger (2013-2015) : initial version
  • Marco Mancini (2015) : parallelization of some computations

REFERENCES:

class sage.manifolds.differentiable.affine_connection.AffineConnection(domain, name, latex_name=None)

Bases: sage.structure.sage_object.SageObject

Affine connection on a smooth manifold.

Let \(M\) be a differentiable manifold of class \(C^\infty\) (smooth manifold) over a non-discrete topological field \(K\) (in most applications \(K=\RR\) or \(K=\CC\)), let \(C^\infty(M)\) be the algebra of smooth functions \(M\rightarrow K\) (cf. DiffScalarFieldAlgebra) and let \(\mathcal{X}(M)\) be the \(C^\infty(M)\)-module of vector fields on \(M\) (cf. VectorFieldModule). An affine connection on \(M\) is an operator

\[\begin{split}\begin{array}{cccc} \nabla: & \mathcal{X}(M)\times \mathcal{X}(M) & \longrightarrow & \mathcal{X}(M) \\ & (u,v) & \longmapsto & \nabla_u v \end{array}\end{split}\]

that

  • is \(K\)-bilinear, i.e. is bilinear when considering \(\mathcal{X}(M)\) as a vector space over \(K\)
  • is \(C^\infty(M)\)-linear w.r.t. the first argument: \(\forall f\in C^\infty(M),\ \nabla_{fu} v = f\nabla_u v\)
  • obeys Leibniz rule w.r.t. the second argument: \(\forall f\in C^\infty(M),\ \nabla_u (f v) = \mathrm{d}f(u)\, v + f \nabla_u v\)

The affine connection \(\nabla\) gives birth to the covariant derivative operator acting on tensor fields, denoted by the same symbol:

\[\begin{split}\begin{array}{cccc} \nabla: & T^{(k,l)}(M) & \longrightarrow & T^{(k,l+1)}(M)\\ & t & \longmapsto & \nabla t \end{array}\end{split}\]

where \(T^{(k,l)}(M)\) stands for the \(C^\infty(M)\)-module of tensor fields of type \((k,l)\) on \(M\) (cf. TensorFieldModule), with the convention \(T^{(0,0)}(M):=C^\infty(M)\). For a vector field \(v\), the covariant derivative \(\nabla v\) is a type-(1,1) tensor field such that

\[\forall u \in\mathcal{X}(M), \ \nabla_u v = \nabla v(., u)\]

More generally for any tensor field \(t\in T^{(k,l)}(M)\), we have

\[\forall u \in\mathcal{X}(M), \ \nabla_u t = \nabla t(\ldots, u)\]

Note

The above convention means that, in terms of index notation, the “derivation index” in \(\nabla t\) is the last one:

\[\nabla_c t^{a_1\ldots a_k}_{\quad\quad b_1\ldots b_l} = (\nabla t)^{a_1\ldots a_k}_{\quad\quad b_1\ldots b_l c}\]

INPUT:

  • domain – the manifold on which the connection is defined (must be an instance of class DifferentiableManifold)
  • name – name given to the affine connection
  • latex_name – (default: None) LaTeX symbol to denote the affine connection; if None, it is set to name.

EXAMPLES:

Affine connection on a 3-dimensional manifold:

sage: M = Manifold(3, 'M', start_index=1)
sage: c_xyz.<x,y,z> = M.chart()
sage: nab = M.affine_connection('nabla', r'\nabla') ; nab
Affine connection nabla on the 3-dimensional differentiable manifold M

A just-created connection has no connection coefficients:

sage: nab._coefficients
{}

The connection coefficients relative to the manifold’s default frame [here \((\partial/\partial x, \partial/\partial y, \partial/\partial z)\)], are created by providing the relevant indices inside square brackets:

sage: nab[1,1,2], nab[3,2,3] = x^2, y*z  # Gamma^1_{12} = x^2, Gamma^3_{23} = yz
sage: nab._coefficients
{Coordinate frame (M, (d/dx,d/dy,d/dz)): 3-indices components w.r.t.
 Coordinate frame (M, (d/dx,d/dy,d/dz))}

If not the default one, the vector frame w.r.t. which the connection coefficients are defined can be specified as the first argument inside the square brackets; hence the above definition is equivalent to:

sage: nab[c_xyz.frame(), 1,1,2], nab[c_xyz.frame(),3,2,3] = x^2, y*z
sage: nab._coefficients
{Coordinate frame (M, (d/dx,d/dy,d/dz)): 3-indices components w.r.t.
 Coordinate frame (M, (d/dx,d/dy,d/dz))}

Unset components are initialized to zero:

sage: nab[:] # list of coefficients relative to the manifold's default vector frame
[[[0, x^2, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, y*z], [0, 0, 0]]]

The treatment of connection coefficients in a given vector frame is similar to that of tensor components; see therefore the class TensorField for the documentation. In particular, the square brackets return the connection coefficients as instances of CoordFunction (of the subclass CoordFunctionSymb in the current example), while the double square brackets return a scalar field:

sage: nab[1,1,2]
x^2
sage: nab[1,1,2].display()
(x, y, z) |--> x^2
sage: type(nab[1,1,2])
<class 'sage.manifolds.coord_func_symb.CoordFunctionSymbRing_with_category.element_class'>
sage: nab[[1,1,2]]
Scalar field on the 3-dimensional differentiable manifold M
sage: nab[[1,1,2]].display()
M --> R
(x, y, z) |--> x^2
sage: nab[[1,1,2]].coord_function() is nab[1,1,2]
True

Action on a scalar field:

sage: f = M.scalar_field(x^2 - y^2, name='f')
sage: Df = nab(f) ; Df
1-form df on the 3-dimensional differentiable manifold M
sage: Df[:]
[2*x, -2*y, 0]

The action of an affine connection connection on a scalar field must coincide with the differential:

sage: Df == f.differential()
True

A generic affine connection has some torsion:

sage: DDf = nab(Df) ; DDf
Tensor field nabla(df) of type (0,2) on the 3-dimensional
 differentiable manifold M
sage: DDf.antisymmetrize()[:] # nabla does not commute on scalar fields:
[   0 -x^3    0]
[ x^3    0    0]
[   0    0    0]

Let us check the standard formula

\[\nabla_j \nabla_i \, f - \nabla_i \nabla_j \, f = T^k_{\ \, ij} \nabla_k \, f ,\]

where the \(T^k_{\ \, ij}\)‘s are the components of the connection’s torsion tensor:

sage: 2*DDf.antisymmetrize() == nab.torsion().contract(0,Df)
True

The connection acting on a vector field:

sage: v = M.vector_field('v')
sage: v[:] = (y*z, x*z, x*y)
sage: Dv = nab(v) ; Dv
Tensor field nabla(v) of type (1,1) on the 3-dimensional differentiable
 manifold M
sage: Dv[:]
[            0 (x^2*y + 1)*z             y]
[            z             0             x]
[            y             x       x*y*z^2]

Another example: connection on a non-parallelizable 2-dimensional manifold:

sage: M = Manifold(2, 'M')
sage: U = M.open_subset('U') ; V = M.open_subset('V')
sage: M.declare_union(U,V)   # M is the union of U and V
sage: c_xy.<x,y> = U.chart() ; c_uv.<u,v> = V.chart()
sage: transf = c_xy.transition_map(c_uv, (x+y, x-y), intersection_name='W',
....:                              restrictions1= x>0, restrictions2= u+v>0)
sage: inv = transf.inverse()
sage: W = U.intersection(V)
sage: eU = c_xy.frame() ; eV = c_uv.frame()
sage: c_xyW = c_xy.restrict(W) ; c_uvW = c_uv.restrict(W)
sage: eUW = c_xyW.frame() ; eVW = c_uvW.frame()
sage: nab = M.affine_connection('nabla', r'\nabla')

The connection is first defined on the open subset U by means of its coefficients w.r.t. the frame eU (the manifold’s default frame):

sage: nab[0,0,0], nab[1,0,1] = x, x*y

The coefficients w.r.t the frame eV are deduced by continuation of the coefficients w.r.t. the frame eVW on the open subset \(W=U\cap V\):

sage: for i in M.irange():
....:     for j in M.irange():
....:         for k in M.irange():
....:             nab.add_coef(eV)[i,j,k] = nab.coef(eVW)[i,j,k,c_uvW].expr()
....:

At this stage, the connection is fully defined on all the manifold:

sage: nab.coef(eU)[:]
[[[x, 0], [0, 0]], [[0, x*y], [0, 0]]]
sage: nab.coef(eV)[:]
[[[1/16*u^2 - 1/16*v^2 + 1/8*u + 1/8*v, -1/16*u^2 + 1/16*v^2 + 1/8*u + 1/8*v],
  [1/16*u^2 - 1/16*v^2 + 1/8*u + 1/8*v, -1/16*u^2 + 1/16*v^2 + 1/8*u + 1/8*v]],
 [[-1/16*u^2 + 1/16*v^2 + 1/8*u + 1/8*v, 1/16*u^2 - 1/16*v^2 + 1/8*u + 1/8*v],
  [-1/16*u^2 + 1/16*v^2 + 1/8*u + 1/8*v, 1/16*u^2 - 1/16*v^2 + 1/8*u + 1/8*v]]]

We may let it act on a vector field defined globally on \(M\):

sage: a = M.vector_field('a')
sage: a[eU,:] = [-y,x]
sage: a[eV,0] = a[eVW,0,c_uvW].expr()
sage: a[eV,1] = a[eVW,1,c_uvW].expr()
sage: a.display(eU)
a = -y d/dx + x d/dy
sage: a.display(eV)
a = v d/du - u d/dv
sage: da = nab(a) ; da
Tensor field nabla(a) of type (1,1) on the 2-dimensional differentiable
 manifold M
sage: da.display(eU)
nabla(a) = -x*y d/dx*dx - d/dx*dy + d/dy*dx - x*y^2 d/dy*dy
sage: da.display(eV)
nabla(a) = (-1/16*u^3 + 1/16*u^2*v + 1/16*(u + 2)*v^2 - 1/16*v^3 - 1/8*u^2) d/du*du
 + (1/16*u^3 - 1/16*u^2*v - 1/16*(u - 2)*v^2 + 1/16*v^3 - 1/8*u^2 + 1) d/du*dv
 + (1/16*u^3 - 1/16*u^2*v - 1/16*(u - 2)*v^2 + 1/16*v^3 - 1/8*u^2 - 1) d/dv*du
 + (-1/16*u^3 + 1/16*u^2*v + 1/16*(u + 2)*v^2 - 1/16*v^3 - 1/8*u^2) d/dv*dv

A few tests:

sage: nab(a.restrict(V)) == da.restrict(V)
True
sage: nab.restrict(V)(a) == da.restrict(V)
True
sage: nab.restrict(V)(a.restrict(U)) == da.restrict(W)
True
sage: nab.restrict(U)(a.restrict(V)) == da.restrict(W)
True
add_coef(frame=None)

Return the connection coefficients in a given frame for assignment, keeping the coefficients in other frames.

See method coef() for details about the definition of the connection coefficents.

To delete the connection coefficients in other frames, use the method set_coef() instead.

INPUT:

  • frame – (default: None) vector frame in which the connection coefficients are defined; if None, the default frame of the connection’s domain is assumed.

Warning

If the connection has already coefficients in other frames, it is the user’s responsibility to make sure that the coefficients to be added are consistent with them.

OUTPUT:

  • connection coefficients in the given frame, as an instance of the class Components; if such connection coefficients did not exist previously, they are created. See method coef() for the storage convention of the connection coefficients.

EXAMPLES:

Setting the coefficients of an affine connection w.r.t. some coordinate frame:

sage: M = Manifold(2, 'M', start_index=1)
sage: X.<x,y> = M.chart()
sage: nab = M.affine_connection('nabla', latex_name=r'\nabla')
sage: eX = X.frame(); eX
Coordinate frame (M, (d/dx,d/dy))
sage: nab.add_coef(eX)
3-indices components w.r.t. Coordinate frame (M, (d/dx,d/dy))
sage: nab.add_coef(eX)[1,2,1] = x*y
sage: nab.display(eX)
Gam^x_yx = x*y

Since eX is the manifold’s default vector frame, its mention may be omitted:

sage: nab.add_coef()[1,2,1] = x*y
sage: nab.add_coef()
3-indices components w.r.t. Coordinate frame (M, (d/dx,d/dy))
sage: nab.add_coef()[1,2,1] = x*y
sage: nab.display()
Gam^x_yx = x*y

Adding connection coefficients w.r.t. to another vector frame:

sage: e = M.vector_frame('e')
sage: nab.add_coef(e)
3-indices components w.r.t. Vector frame (M, (e_1,e_2))
sage: nab.add_coef(e)[2,1,1] = x+y
sage: nab.add_coef(e)[2,1,2] = x-y
sage: nab.display(e)
Gam^2_11 = x + y
Gam^2_12 = x - y

The coefficients w.r.t. the frame eX have been kept:

sage: nab.display(eX)
Gam^x_yx = x*y

To delete them, use the method set_coef() instead.

coef(frame=None)

Return the connection coefficients relative to the given frame.

\(n\) being the manifold’s dimension, the connection coefficients relative to the vector frame \((e_i)\) are the \(n^3\) scalar fields \(\Gamma^k_{\ \, ij}\) defined by

\[\nabla_{e_j} e_i = \Gamma^k_{\ \, ij} e_k\]

If the connection coefficients are not known already, they are computed from the above formula.

INPUT:

  • frame – (default: None) vector frame relative to which the connection coefficients are required; if none is provided, the domain’s default frame is assumed

OUTPUT:

  • connection coefficients relative to the frame frame, as an instance of the class Components with 3 indices ordered as \((k,i,j)\)

EXAMPLES:

Connection coefficient of an affine connection on a 3-dimensional manifold:

sage: M = Manifold(3, 'M', start_index=1)
sage: c_xyz.<x,y,z> = M.chart()
sage: nab = M.affine_connection('nabla', r'\nabla')
sage: nab[1,1,2], nab[3,2,3] = x^2, y*z  # Gamma^1_{12} = x^2, Gamma^3_{23} = yz
sage: nab.coef()
3-indices components w.r.t. Coordinate frame (M, (d/dx,d/dy,d/dz))
sage: type(nab.coef())
<class 'sage.tensor.modules.comp.Components'>
sage: M.default_frame()
Coordinate frame (M, (d/dx,d/dy,d/dz))
sage: nab.coef() is nab.coef(c_xyz.frame())
True
sage: nab.coef()[:]  # full list of coefficients:
[[[0, x^2, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, y*z], [0, 0, 0]]]
connection_form(i, j, frame=None)

Return the connection 1-form corresponding to the given index and vector frame.

The connection 1-forms with respect to the frame \((e_i)\) are the \(n^2\) 1-forms \(\omega^i_{\ \, j}\) defined by

\[\nabla_v e_j = \langle \omega^i_{\ \, j}, v \rangle \, e_i\]

for any vector \(v\).

The components of \(\omega^i_{\ \, j}\) in the coframe \((e^i)\) dual to \((e_i)\) are nothing but the connection coefficients \(\Gamma^i_{\ \, jk}\) relative to the frame \((e_i)\):

\[\omega^i_{\ \, j} = \Gamma^i_{\ \, jk} e^k\]

INPUT:

  • i, j – indices identifying the 1-form \(\omega^i_{\ \, j}\)
  • frame – (default: None) vector frame relative to which the connection 1-forms are defined; if None, the default frame of the connection’s domain is assumed.

OUTPUT:

  • the 1-form \(\omega^i_{\ \, j}\), as an instance of DiffForm

EXAMPLES:

Connection 1-forms on a 3-dimensional manifold:

sage: M = Manifold(3, 'M', start_index=1)
sage: c_xyz.<x,y,z> = M.chart()
sage: nab = M.affine_connection('nabla', r'\nabla')
sage: nab[1,1,1], nab[1,1,2], nab[1,1,3] = x*y*z, x^2, -y*z
sage: nab[1,2,3], nab[1,3,1], nab[1,3,2] = -x^3, y^2*z, y^2-x^2
sage: nab[2,1,1], nab[2,1,2], nab[2,2,1] = z^2, x*y*z^2, -x^2
sage: nab[2,3,1], nab[2,3,3], nab[3,1,2] = x^2+y^2+z^2, y^2-z^2, x*y+z^2
sage: nab[3,2,1], nab[3,2,2], nab[3,3,3] = x*y+z, z^3 -y^2, x*z^2 - z*y^2
sage: nab.connection_form(1,1)  # connection 1-form (i,j)=(1,1) w.r.t. M's default frame
1-form nabla connection 1-form (1,1) on the 3-dimensional
 differentiable manifold M
sage: nab.connection_form(1,1)[:]
[x*y*z, x^2, -y*z]

The result is cached (until the connection is modified via set_coef() or add_coef()):

sage: nab.connection_form(1,1) is nab.connection_form(1,1)
True

Connection 1-forms w.r.t. a non-holonomic frame:

sage: ch_basis = M.automorphism_field()
sage: ch_basis[1,1], ch_basis[2,2], ch_basis[3,3] = y, z, x
sage: e = M.default_frame().new_frame(ch_basis, 'e')
sage: e[1][:], e[2][:], e[3][:]
([y, 0, 0], [0, z, 0], [0, 0, x])
sage: nab.connection_form(1,1,e)
1-form nabla connection 1-form (1,1) on the 3-dimensional
 differentiable manifold M
sage: nab.connection_form(1,1,e).comp(e)[:]
[x*y^2*z, (x^2*y + 1)*z/y, -x*y*z]

Check of the formula \(\omega^i_{\ \, j} = \Gamma^i_{\ \, jk} e^k\):

sage: #... on the manifold's default frame (d/dx, d/dy, d:dz)
sage: dx = M.default_frame().coframe() ; dx
Coordinate coframe (M, (dx,dy,dz))
sage: check = []
sage: for i in M.irange():
....:     for j in M.irange():
....:         check.append( nab.connection_form(i,j) == \
....:               sum( nab[[i,j,k]]*dx[k] for k in M.irange() ) )
....:
sage: check
[True, True, True, True, True, True, True, True, True]
sage: #... on the frame e
sage: ef = e.coframe() ; ef
Coframe (M, (e^1,e^2,e^3))
sage: check = []
sage: for i in M.irange():
....:     for j in M.irange():
....:         s = nab.connection_form(i,j,e).comp(c_xyz.frame(), from_basis=e)
....:         check.append( nab.connection_form(i,j,e) == sum( nab.coef(e)[[i,j,k]]*ef[k] for k in M.irange() ) )
....:
sage: check
[True, True, True, True, True, True, True, True, True]

Check of the formula \(\nabla_v e_j = \langle \omega^i_{\ \, j}, v \rangle e_i\):

sage: v = M.vector_field()
sage: v[:] = (x*y, z^2-3*x, z+2*y)
sage: b = M.default_frame()
sage: for j in M.irange():  # check on M's default frame
....:     nab(b[j]).contract(v) == \
....:      sum( nab.connection_form(i,j)(v)*b[i] for i in M.irange())
True
True
True
sage: for j in M.irange():  # check on frame e
....:     nab(e[j]).contract(v) == \
....:      sum( nab.connection_form(i,j,e)(v)*e[i] for i in M.irange())
True
True
True
curvature_form(i, j, frame=None)

Return the curvature 2-form corresponding to the given index and vector frame.

The curvature 2-forms with respect to the frame \((e_i)\) are the \(n^2\) 2-forms \(\Omega^i_{\ \, j}\) defined by

\[\Omega^i_{\ \, j}(u,v) = R(e^i, e_j, u, v)\]

where \(R\) is the connection’s Riemann curvature tensor (cf. riemann()), \((e^i)\) is the coframe dual to \((e_i)\) and \((u,v)\) is a generic pair of vectors.

INPUT:

  • i, j – indices identifying the 2-form \(\Omega^i_{\ \, j}\)
  • frame – (default: None) vector frame relative to which the curvature 2-forms are defined; if None, the default frame of the connection’s domain is assumed.

OUTPUT:

  • the 2-form \(\Omega^i_{\ \, j}\), as an instance of DiffForm

EXAMPLES:

Curvature 2-forms on a 3-dimensional manifold:

sage: M = Manifold(3, 'M', start_index=1)
sage: c_xyz.<x,y,z> = M.chart()
sage: nab = M.affine_connection('nabla', r'\nabla')
sage: nab[1,1,1], nab[1,1,2], nab[1,1,3] = x*y*z, x^2, -y*z
sage: nab[1,2,3], nab[1,3,1], nab[1,3,2] = -x^3, y^2*z, y^2-x^2
sage: nab[2,1,1], nab[2,1,2], nab[2,2,1] = z^2, x*y*z^2, -x^2
sage: nab[2,3,1], nab[2,3,3], nab[3,1,2] = x^2+y^2+z^2, y^2-z^2, x*y+z^2
sage: nab[3,2,1], nab[3,2,2], nab[3,3,3] = x*y+z, z^3 -y^2, x*z^2 - z*y^2
sage: nab.curvature_form(1,1)  # long time
2-form curvature (1,1) of connection nabla w.r.t. Coordinate frame
 (M, (d/dx,d/dy,d/dz)) on the 3-dimensional differentiable manifold M
sage: nab.curvature_form(1,1).display()  # long time (if above is skipped)
curvature (1,1) of connection nabla w.r.t. Coordinate frame
 (M, (d/dx,d/dy,d/dz)) = (y^2*z^3 + (x*y^3 - x)*z + 2*x) dx/\dy
  + (x^3*z^2 - x*y) dx/\dz + (x^4*y*z^2 - z) dy/\dz

Curvature 2-forms w.r.t. a non-holonomic frame:

sage: ch_basis = M.automorphism_field()
sage: ch_basis[1,1], ch_basis[2,2], ch_basis[3,3] = y, z, x
sage: e = M.default_frame().new_frame(ch_basis, 'e')
sage: e[1].display(), e[2].display(), e[3].display()
(e_1 = y d/dx, e_2 = z d/dy, e_3 = x d/dz)
sage: ef = e.coframe()
sage: ef[1].display(), ef[2].display(), ef[3].display()
(e^1 = 1/y dx, e^2 = 1/z dy, e^3 = 1/x dz)
sage: nab.curvature_form(1,1,e)  # long time
2-form curvature (1,1) of connection nabla w.r.t. Vector frame
 (M, (e_1,e_2,e_3)) on the 3-dimensional differentiable manifold M
sage: nab.curvature_form(1,1,e).display(e)  # long time (if above is skipped)
 curvature (1,1) of connection nabla w.r.t. Vector frame
 (M, (e_1,e_2,e_3)) =
  (y^3*z^4 + 2*x*y*z + (x*y^4 - x*y)*z^2) e^1/\e^2
  + (x^4*y*z^2 - x^2*y^2) e^1/\e^3 + (x^5*y*z^3 - x*z^2) e^2/\e^3

Cartan’s second structure equation is

\[\Omega^i_{\ \, j} = \mathrm{d} \omega^i_{\ \, j} + \omega^i_{\ \, k} \wedge \omega^k_{\ \, j}\]

where the \(\omega^i_{\ \, j}\)‘s are the connection 1-forms (cf. connection_form()). Let us check it on the frame e:

sage: omega = nab.connection_form
sage: check = []
sage: for i in M.irange():  # long time
....:     for j in M.irange():
....:         check.append( nab.curvature_form(i,j,e) == \
....:                       omega(i,j,e).exterior_derivative() + \
....:         sum( omega(i,k,e).wedge(omega(k,j,e)) for k in M.irange()) )
....:
sage: check  # long time
[True, True, True, True, True, True, True, True, True]
del_other_coef(frame=None)

Delete all the coefficients but those corresponding to frame.

INPUT:

  • frame – (default: None) vector frame, the connection coefficients w.r.t. which are to be kept; if None, the default frame of the connection’s domain is assumed.

EXAMPLES:

We first create two sets of connection coefficients:

sage: M = Manifold(2, 'M', start_index=1)
sage: X.<x,y> = M.chart()
sage: nab = M.affine_connection('nabla', latex_name=r'\nabla')
sage: eX = X.frame()
sage: nab.set_coef(eX)[1,2,1] = x*y
sage: e = M.vector_frame('e')
sage: nab.add_coef(e)[2,1,1] = x+y
sage: nab.display(eX)
Gam^x_yx = x*y
sage: nab.display(e)
Gam^2_11 = x + y

Let us delete the connection coefficients w.r.t. all frames except for frame eX:

sage: nab.del_other_coef(eX)
sage: nab.display(eX)
Gam^x_yx = x*y

The connection coefficients w.r.t. frame e have indeed been deleted:

sage: nab.display(e)
Traceback (most recent call last):
...
ValueError: no common frame found for the computation
display(frame=None, chart=None, symbol=None, latex_symbol=None, index_labels=None, index_latex_labels=None, coordinate_labels=True, only_nonzero=True, only_nonredundant=False)

Display all the connection coefficients w.r.t. to a given frame, one per line.

The output is either text-formatted (console mode) or LaTeX-formatted (notebook mode).

INPUT:

  • frame – (default: None) vector frame relative to which the connection coefficients are defined; if None, the default frame of the connection’s domain is used
  • chart – (default: None) chart specifying the coordinate expression of the connection coefficients; if None, the default chart of the connection’s domain is used
  • symbol – (default: None) string specifying the symbol of the connection coefficients; if None, ‘Gam’ is used
  • latex_symbol – (default: None) string specifying the LaTeX symbol for the components; if None, ‘\Gamma’ is used
  • index_labels – (default: None) list of strings representing the labels of each index; if None, integer labels are used, except if frame is a coordinate frame and coordinate_symbols is set to True, in which case the coordinate symbols are used
  • index_latex_labels – (default: None) list of strings representing the LaTeX labels of each index; if None, integer labels are used, except if frame is a coordinate frame and coordinate_symbols is set to True, in which case the coordinate LaTeX symbols are used
  • coordinate_labels – (default: True) boolean; if True, coordinate symbols are used by default (instead of integers) as index labels whenever frame is a coordinate frame
  • only_nonzero – (default: True) boolean; if True, only nonzero connection coefficients are displayed
  • only_nonredundant – (default: False) boolean; if True, only nonredundant connection coefficients are displayed in case of symmetries

EXAMPLES:

Coefficients of a connection on a 3-dimensional manifold:

sage: M = Manifold(3, 'M', start_index=1)
sage: c_xyz.<x,y,z> = M.chart()
sage: nab = M.affine_connection('nabla', r'\nabla')
sage: nab[1,1,2], nab[3,2,3] = x^2, y*z

By default, only the nonzero connection coefficients are displayed:

sage: nab.display()
Gam^x_xy = x^2
Gam^z_yz = y*z
sage: latex(nab.display())
\begin{array}{lcl} \Gamma_{ \phantom{\, x} \, x \, y }^{ \, x \phantom{\, x} \phantom{\, y} }
& = & x^{2} \\
\Gamma_{ \phantom{\, z} \, y \, z }^{ \, z \phantom{\, y} \phantom{\, z} }
& = & y z \end{array}

By default, the displayed connection coefficients are those w.r.t. to the default frame of the connection’s domain, so the above is equivalent to:

sage: nab.display(frame=M.default_frame())
Gam^x_xy = x^2
Gam^z_yz = y*z

Since the default frame is a coordinate frame, coordinate symbols are used to label the indices, but one may ask for integers instead:

sage: M.default_frame() is c_xyz.frame()
True
sage: nab.display(coordinate_labels=False)
Gam^1_12 = x^2
Gam^3_23 = y*z

The index labels can also be customized:

sage: nab.display(index_labels=['(1)', '(2)', '(3)'])
Gam^(1)_(1),(2) = x^2
Gam^(3)_(2),(3) = y*z

The symbol ‘Gam’ can be changed:

sage: nab.display(symbol='C', latex_symbol='C')
C^x_xy = x^2
C^z_yz = y*z
sage: latex(nab.display(symbol='C', latex_symbol='C'))
\begin{array}{lcl} C_{ \phantom{\, x} \, x \, y }^{ \, x \phantom{\, x} \phantom{\, y} }
& = & x^{2} \\
C_{ \phantom{\, z} \, y \, z }^{ \, z \phantom{\, y} \phantom{\, z} }
& = & y z \end{array}

Display of Christoffel symbols, skipping the redundancy associated with the symmetry of the last two indices:

sage: M = Manifold(3, 'R^3', start_index=1)
sage: c_spher.<r,th,ph> = M.chart(r'r:(0,+oo) th:(0,pi):\theta ph:(0,2*pi):\phi')
sage: g = M.metric('g')
sage: g[1,1], g[2,2], g[3,3] = 1, r^2 , (r*sin(th))^2
sage: g.display()
g = dr*dr + r^2 dth*dth + r^2*sin(th)^2 dph*dph
sage: g.connection().display(only_nonredundant=True)
Gam^r_th,th = -r
Gam^r_ph,ph = -r*sin(th)^2
Gam^th_r,th = 1/r
Gam^th_ph,ph = -cos(th)*sin(th)
Gam^ph_r,ph = 1/r
Gam^ph_th,ph = cos(th)/sin(th)

By default, the parameter only_nonredundant is set to False:

sage: g.connection().display()
Gam^r_th,th = -r
Gam^r_ph,ph = -r*sin(th)^2
Gam^th_r,th = 1/r
Gam^th_th,r = 1/r
Gam^th_ph,ph = -cos(th)*sin(th)
Gam^ph_r,ph = 1/r
Gam^ph_th,ph = cos(th)/sin(th)
Gam^ph_ph,r = 1/r
Gam^ph_ph,th = cos(th)/sin(th)
domain()

Return the manifold subset on which the affine connection is defined.

OUTPUT:

EXAMPLES:

sage: M = Manifold(3, 'M', start_index=1)
sage: c_xyz.<x,y,z> = M.chart()
sage: nab = M.affine_connection('nabla', r'\nabla')
sage: nab.domain()
3-dimensional differentiable manifold M
sage: U = M.open_subset('U', coord_def={c_xyz: x>0})
sage: nabU = U.affine_connection('D')
sage: nabU.domain()
Open subset U of the 3-dimensional differentiable manifold M
restrict(subdomain)

Return the restriction of the connection to some subdomain.

If such restriction has not been defined yet, it is constructed here.

INPUT:

  • subdomain – open subset \(U\) of the connection’s domain (must be an instance of DifferentiableManifold)

OUTPUT:

EXAMPLES:

Restriction of a connection on a 2-dimensional manifold:

sage: M = Manifold(2, 'M', start_index=1)
sage: c_xy.<x,y> = M.chart()
sage: nab = M.affine_connection('nabla', r'\nabla')
sage: nab[1,1,2], nab[2,1,1] = x^2, x+y
sage: nab[:]
[[[0, x^2], [0, 0]], [[x + y, 0], [0, 0]]]
sage: U = M.open_subset('U', coord_def={c_xy: x>0})
sage: nabU = nab.restrict(U) ; nabU
Affine connection nabla on the Open subset U of the 2-dimensional
 differentiable manifold M
sage: nabU.domain()
Open subset U of the 2-dimensional differentiable manifold M
sage: nabU[:]
[[[0, x^2], [0, 0]], [[x + y, 0], [0, 0]]]

The result is cached:

sage: nab.restrict(U) is nabU
True

until the connection is modified:

sage: nab[1,2,2] = -y
sage: nab.restrict(U) is nabU
False
sage: nab.restrict(U)[:]
[[[0, x^2], [0, -y]], [[x + y, 0], [0, 0]]]
ricci()

Return the connection’s Ricci tensor.

The Ricci tensor is the tensor field \(Ric\) of type (0,2) defined from the Riemann curvature tensor \(R\) by

\[Ric(u, v) = R(e^i, u, e_i, v)\]

for any vector fields \(u\) and \(v\), \((e_i)\) being any vector frame and \((e^i)\) the dual coframe.

OUTPUT:

  • the Ricci tensor \(Ric\), as an instance of TensorField

EXAMPLES:

Ricci tensor of an affine connection on a 3-dimensional manifold:

sage: M = Manifold(3, 'M', start_index=1)
sage: c_xyz.<x,y,z> = M.chart()
sage: nab = M.affine_connection('nabla', r'\nabla') ; nab
Affine connection nabla on the 3-dimensional differentiable
 manifold M
sage: nab[1,1,2], nab[3,2,3] = x^2, y*z  # Gamma^1_{12} = x^2, Gamma^3_{23} = yz
sage: r = nab.ricci() ; r
Tensor field of type (0,2) on the 3-dimensional differentiable
 manifold M
sage: r[:]
[  0 2*x   0]
[  0  -z   0]
[  0   0   0]

The result is cached (until the connection is modified via set_coef() or add_coef()):

sage: nab.ricci() is r
True
riemann()

Return the connection’s Riemann curvature tensor.

The Riemann curvature tensor is the tensor field \(R\) of type (1,3) defined by

\[R(\omega, w, u, v) = \left\langle \omega, \nabla_u \nabla_v w - \nabla_v \nabla_u w - \nabla_{[u, v]} w \right\rangle\]

for any 1-form \(\omega\) and any vector fields \(u\), \(v\) and \(w\).

OUTPUT:

  • the Riemann curvature tensor \(R\), as an instance of TensorField

EXAMPLES:

Curvature of an affine connection on a 3-dimensional manifold:

sage: M = Manifold(3, 'M', start_index=1)
sage: c_xyz.<x,y,z> = M.chart()
sage: nab = M.affine_connection('nabla', r'\nabla') ; nab
Affine connection nabla on the 3-dimensional differentiable
 manifold M
sage: nab[1,1,2], nab[3,2,3] = x^2, y*z  # Gamma^1_{12} = x^2, Gamma^3_{23} = yz
sage: r = nab.riemann() ; r
Tensor field of type (1,3) on the 3-dimensional differentiable
 manifold M
sage: r.parent()
Free module T^(1,3)(M) of type-(1,3) tensors fields on the
 3-dimensional differentiable manifold M

By construction, the Riemann tensor is antisymmetric with respect to its last two arguments (denoted \(u\) and \(v\) in the definition above), which are at positions 2 and 3 (the first argument being at position 0):

sage: r.symmetries()
no symmetry;  antisymmetry: (2, 3)

The components:

sage: r[:]
[[[[0, 2*x, 0], [-2*x, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]],
[[[0, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]],
[[[0, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, z], [0, -z, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]]]

The result is cached (until the connection is modified via set_coef() or add_coef()):

sage: nab.riemann() is r
True

Another example: Riemann curvature tensor of some connection on a non-parallelizable 2-dimensional manifold:

sage: M = Manifold(2, 'M')
sage: U = M.open_subset('U') ; V = M.open_subset('V')
sage: M.declare_union(U,V)   # M is the union of U and V
sage: c_xy.<x,y> = U.chart() ; c_uv.<u,v> = V.chart()
sage: transf = c_xy.transition_map(c_uv, (x+y, x-y), intersection_name='W',
....:                              restrictions1= x>0, restrictions2= u+v>0)
sage: inv = transf.inverse()
sage: W = U.intersection(V)
sage: eU = c_xy.frame() ; eV = c_uv.frame()
sage: c_xyW = c_xy.restrict(W) ; c_uvW = c_uv.restrict(W)
sage: eUW = c_xyW.frame() ; eVW = c_uvW.frame()
sage: nab = M.affine_connection('nabla', r'\nabla')
sage: nab[0,0,0], nab[0,1,0], nab[1,0,1] = x, x-y, x*y
sage: for i in M.irange():
....:     for j in M.irange():
....:         for k in M.irange():
....:             nab.add_coef(eV)[i,j,k] = nab.coef(eVW)[i,j,k,c_uvW].expr()
....:
sage: r = nab.riemann() ; r
Tensor field of type (1,3) on the 2-dimensional differentiable
 manifold M
sage: r.parent()
Module T^(1,3)(M) of type-(1,3) tensors fields on the 2-dimensional
 differentiable manifold M
sage: r.display(eU)
(x^2*y - x*y^2) d/dx*dx*dx*dy + (-x^2*y + x*y^2) d/dx*dx*dy*dx + d/dx*dy*dx*dy
 - d/dx*dy*dy*dx - (x^2 - 1)*y d/dy*dx*dx*dy + (x^2 - 1)*y d/dy*dx*dy*dx
 + (-x^2*y + x*y^2) d/dy*dy*dx*dy + (x^2*y - x*y^2) d/dy*dy*dy*dx
sage: r.display(eV)
(1/32*u^3 - 1/32*u*v^2 - 1/32*v^3 + 1/32*(u^2 + 4)*v - 1/8*u - 1/4) d/du*du*du*dv
 + (-1/32*u^3 + 1/32*u*v^2 + 1/32*v^3 - 1/32*(u^2 + 4)*v + 1/8*u + 1/4) d/du*du*dv*du
 + (1/32*u^3 - 1/32*u*v^2 + 3/32*v^3 - 1/32*(3*u^2 - 4)*v - 1/8*u + 1/4) d/du*dv*du*dv
 + (-1/32*u^3 + 1/32*u*v^2 - 3/32*v^3 + 1/32*(3*u^2 - 4)*v + 1/8*u - 1/4) d/du*dv*dv*du
 + (-1/32*u^3 + 1/32*u*v^2 + 5/32*v^3 - 1/32*(5*u^2 + 4)*v + 1/8*u - 1/4) d/dv*du*du*dv
 + (1/32*u^3 - 1/32*u*v^2 - 5/32*v^3 + 1/32*(5*u^2 + 4)*v - 1/8*u + 1/4) d/dv*du*dv*du
 + (-1/32*u^3 + 1/32*u*v^2 + 1/32*v^3 - 1/32*(u^2 + 4)*v + 1/8*u + 1/4) d/dv*dv*du*dv
 + (1/32*u^3 - 1/32*u*v^2 - 1/32*v^3 + 1/32*(u^2 + 4)*v - 1/8*u - 1/4) d/dv*dv*dv*du

The same computation parallelized on 2 cores:

sage: Parallelism().set(nproc=2)
sage: r_backup = r
sage: nab = M.affine_connection('nabla', r'\nabla')
sage: nab[0,0,0], nab[0,1,0], nab[1,0,1] = x, x-y, x*y
sage: for i in M.irange():
....:     for j in M.irange():
....:         for k in M.irange():
....:             nab.add_coef(eV)[i,j,k] = nab.coef(eVW)[i,j,k,c_uvW].expr()
....:
sage: r = nab.riemann() ; r
Tensor field of type (1,3) on the 2-dimensional differentiable
 manifold M
sage: r.parent()
Module T^(1,3)(M) of type-(1,3) tensors fields on the 2-dimensional
 differentiable manifold M
sage: r == r_backup
True
sage: Parallelism().set(nproc=1)  # switch off parallelization
set_coef(frame=None)

Return the connection coefficients in a given frame for assignment.

See method coef() for details about the definition of the connection coefficents.

The connection coefficients with respect to other frames are deleted, in order to avoid any inconsistency. To keep them, use the method add_coef() instead.

INPUT:

  • frame – (default: None) vector frame in which the connection coefficients are defined; if None, the default frame of the connection’s domain is assumed.

OUTPUT:

  • connection coefficients in the given frame, as an instance of the class Components; if such connection coefficients did not exist previously, they are created. See method coef() for the storage convention of the connection coefficients.

EXAMPLES:

Setting the coefficients of an affine connection w.r.t. some coordinate frame:

sage: M = Manifold(2, 'M', start_index=1)
sage: X.<x,y> = M.chart()
sage: nab = M.affine_connection('nabla', latex_name=r'\nabla')
sage: eX = X.frame(); eX
Coordinate frame (M, (d/dx,d/dy))
sage: nab.set_coef(eX)
3-indices components w.r.t. Coordinate frame (M, (d/dx,d/dy))
sage: nab.set_coef(eX)[1,2,1] = x*y
sage: nab.display(eX)
Gam^x_yx = x*y

Since eX is the manifold’s default vector frame, its mention may be omitted:

sage: nab.set_coef()[1,2,1] = x*y
sage: nab.set_coef()
3-indices components w.r.t. Coordinate frame (M, (d/dx,d/dy))
sage: nab.set_coef()[1,2,1] = x*y
sage: nab.display()
Gam^x_yx = x*y

To set the coefficients in the default frame, one can even bypass the method set_coef() and call directly the operator [] on the connection object:

sage: nab[1,2,1] = x*y
sage: nab.display()
    Gam^x_yx = x*y

Setting the connection coefficients w.r.t. to another vector frame:

sage: e = M.vector_frame('e')
sage: nab.set_coef(e)
3-indices components w.r.t. Vector frame (M, (e_1,e_2))
sage: nab.set_coef(e)[2,1,1] = x+y
sage: nab.set_coef(e)[2,1,2] = x-y
sage: nab.display(e)
Gam^2_11 = x + y
Gam^2_12 = x - y

The coefficients w.r.t. the frame eX have been deleted:

sage: nab.display(eX)
Traceback (most recent call last):
...
ValueError: no common frame found for the computation

To keep them, use the method add_coef() instead.

torsion()

Return the connection’s torsion tensor.

The torsion tensor is the tensor field \(T\) of type (1,2) defined by

\[T(\omega, u, v) = \left\langle \omega, \nabla_u v - \nabla_v u - [u, v] \right\rangle\]

for any 1-form \(\omega\) and any vector fields \(u\) and \(v\).

OUTPUT:

  • the torsion tensor \(T\), as an instance of TensorField

EXAMPLES:

Torsion of an affine connection on a 3-dimensional manifold:

sage: M = Manifold(3, 'M', start_index=1)
sage: c_xyz.<x,y,z> = M.chart()
sage: nab = M.affine_connection('nabla', r'\nabla')
sage: nab[1,1,2], nab[3,2,3] = x^2, y*z  # Gamma^1_{12} = x^2, Gamma^3_{23} = yz
sage: t = nab.torsion() ; t
Tensor field of type (1,2) on the 3-dimensional differentiable
 manifold M
sage: t.symmetries()
no symmetry;  antisymmetry: (1, 2)
sage: t[:]
[[[0, -x^2, 0], [x^2, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, -y*z], [0, y*z, 0]]]

The torsion expresses the lack of commutativity of two successive derivatives of a scalar field:

sage: f = M.scalar_field(x*z^2 + y^2 - z^2, name='f')
sage: DDf = nab(nab(f)) ; DDf
Tensor field nabla(df) of type (0,2) on the 3-dimensional
 differentiable manifold M
sage: DDf.antisymmetrize()[:]  # two successive derivatives do not commute:
[             0   -1/2*x^2*z^2              0]
[   1/2*x^2*z^2              0 -(x - 1)*y*z^2]
[             0  (x - 1)*y*z^2              0]
sage: 2*DDf.antisymmetrize() == nab.torsion().contract(0,nab(f))
True

The above identity is the standard formula

\[\nabla_j \nabla_i \, f - \nabla_i \nabla_j \, f = T^k_{\ \, ij} \nabla_k \, f ,\]

where the \(T^k_{\ \, ij}\)‘s are the components of the torsion tensor.

The result is cached:

sage: nab.torsion() is t
True

as long as the connection remains unchanged:

sage: nab[2,1,3] = 1+x    # changing the connection
sage: nab.torsion() is t  # a new computation of the torsion has been made
False
sage: (nab.torsion() - t).display()
(-x - 1) d/dy*dx*dz + (x + 1) d/dy*dz*dx

Another example: torsion of some connection on a non-parallelizable 2-dimensional manifold:

sage: M = Manifold(2, 'M')
sage: U = M.open_subset('U') ; V = M.open_subset('V')
sage: M.declare_union(U,V)   # M is the union of U and V
sage: c_xy.<x,y> = U.chart() ; c_uv.<u,v> = V.chart()
sage: transf = c_xy.transition_map(c_uv, (x+y, x-y), intersection_name='W',
....:                              restrictions1= x>0, restrictions2= u+v>0)
sage: inv = transf.inverse()
sage: W = U.intersection(V)
sage: eU = c_xy.frame() ; eV = c_uv.frame()
sage: c_xyW = c_xy.restrict(W) ; c_uvW = c_uv.restrict(W)
sage: eUW = c_xyW.frame() ; eVW = c_uvW.frame()
sage: nab = M.affine_connection('nabla', r'\nabla')
sage: nab[0,0,0], nab[0,1,0], nab[1,0,1] = x, x-y, x*y
sage: for i in M.irange():
....:     for j in M.irange():
....:         for k in M.irange():
....:             nab.add_coef(eV)[i,j,k] = nab.coef(eVW)[i,j,k,c_uvW].expr()
....:
sage: t = nab.torsion() ; t
Tensor field of type (1,2) on the 2-dimensional differentiable
 manifold M
sage: t.parent()
Module T^(1,2)(M) of type-(1,2) tensors fields on the 2-dimensional
 differentiable manifold M
sage: t[eU,:]
[[[0, x - y], [-x + y, 0]], [[0, -x*y], [x*y, 0]]]
sage: t[eV,:]
[[[0, 1/8*u^2 - 1/8*v^2 - 1/2*v], [-1/8*u^2 + 1/8*v^2 + 1/2*v, 0]],
 [[0, -1/8*u^2 + 1/8*v^2 - 1/2*v], [1/8*u^2 - 1/8*v^2 + 1/2*v, 0]]]

Check of the torsion formula:

sage: f = M.scalar_field({c_xy: (x+y)^2, c_uv: u^2}, name='f')
sage: DDf = nab(nab(f)) ; DDf
Tensor field nabla(df) of type (0,2) on the 2-dimensional
 differentiable manifold M
sage: DDf.antisymmetrize().display(eU)
(-x^2*y - (x + 1)*y^2 + x^2) dx/\dy
sage: DDf.antisymmetrize().display(eV)
(1/8*u^3 - 1/8*u*v^2 - 1/2*u*v) du/\dv
sage: 2*DDf.antisymmetrize() == nab(f).contract(nab.torsion())
True
torsion_form(i, frame=None)

Return the torsion 2-form corresponding to the given index and vector frame.

The torsion 2-forms with respect to the frame \((e_i)\) are the \(n\) 2-forms \(\theta^i\) defined by

\[\theta^i(u,v) = T(e^i, u, v)\]

where \(T\) is the connection’s torsion tensor (cf. torsion()), \((e^i)\) is the coframe dual to \((e_i)\) and \((u,v)\) is a generic pair of vectors.

INPUT:

  • i – index identifying the 2-form \(\theta^i\)
  • frame – (default: None) vector frame relative to which the torsion 2-forms are defined; if None, the default frame of the connection’s domain is assumed.

OUTPUT:

  • the 2-form \(\theta^i\), as an instance of DiffForm

EXAMPLES:

Torsion 2-forms on a 3-dimensional manifold:

sage: M = Manifold(3, 'M', start_index=1)
sage: c_xyz.<x,y,z> = M.chart()
sage: nab = M.affine_connection('nabla', r'\nabla')
sage: nab[1,1,1], nab[1,1,2], nab[1,1,3] = x*y*z, x^2, -y*z
sage: nab[1,2,3], nab[1,3,1], nab[1,3,2] = -x^3, y^2*z, y^2-x^2
sage: nab[2,1,1], nab[2,1,2], nab[2,2,1] = z^2, x*y*z^2, -x^2
sage: nab[2,3,1], nab[2,3,3], nab[3,1,2] = x^2+y^2+z^2, y^2-z^2, x*y+z^2
sage: nab[3,2,1], nab[3,2,2], nab[3,3,3] = x*y+z, z^3 -y^2, x*z^2 - z*y^2
sage: nab.torsion_form(1)
2-form torsion (1) of connection nabla w.r.t. Coordinate frame
 (M, (d/dx,d/dy,d/dz)) on the 3-dimensional differentiable manifold M
sage: nab.torsion_form(1)[:]
[               0             -x^2      (y^2 + y)*z]
[             x^2                0  x^3 - x^2 + y^2]
[    -(y^2 + y)*z -x^3 + x^2 - y^2                0]

Torsion 2-forms w.r.t. a non-holonomic frame:

sage: ch_basis = M.automorphism_field()
sage: ch_basis[1,1], ch_basis[2,2], ch_basis[3,3] = y, z, x
sage: e = M.default_frame().new_frame(ch_basis, 'e')
sage: e[1][:], e[2][:], e[3][:]
([y, 0, 0], [0, z, 0], [0, 0, x])
sage: ef = e.coframe()
sage: ef[1][:], ef[2][:], ef[3][:]
([1/y, 0, 0], [0, 1/z, 0], [0, 0, 1/x])
sage: nab.torsion_form(1, e)
2-form torsion (1) of connection nabla w.r.t. Vector frame
 (M, (e_1,e_2,e_3)) on the 3-dimensional differentiable manifold M
sage: nab.torsion_form(1, e).comp(e)[:]
[                       0                   -x^2*z          (x*y^2 + x*y)*z]
[                   x^2*z                        0  (x^4 - x^3 + x*y^2)*z/y]
[        -(x*y^2 + x*y)*z -(x^4 - x^3 + x*y^2)*z/y                        0]

Cartan’s first structure equation is

\[\theta^i = \mathrm{d} e^i + \omega^i_{\ \, j} \wedge e^j\]

where the \(\omega^i_{\ \, j}\)‘s are the connection 1-forms (cf. connection_form()). Let us check it on the frame e:

sage: for i in M.irange():  # long time
....:     nab.torsion_form(i, e) == ef[i].exterior_derivative() + \
....:      sum(nab.connection_form(i,j,e).wedge(ef[j]) for j in M.irange())
....:
True
True
True