Open with one click!

Smooth manifolds, charts and scalar fields

This woksheet accompanies the lecture Symbolic tensor calculus on manifolds at JNCF 2018.

Click here to download the worksheet file (ipynb format). To run it, you must start SageMath with the Jupyter notebook, via the command sage -n jupyter

NB: a version of SageMath at least equal to 7.5 (8.2 for the SymPy part) is required to run this worksheet:

In [1]:
'SageMath version 8.4, Release Date: 2018-10-17'

First we set up the notebook to use LaTeX display:

In [2]:
%display latex

Starting with a manifold

Manifolds are constructed via the global function Manifold:

In [3]:
M = Manifold(2, 'M') print(M)
2-dimensional differentiable manifold M

By default, Manifold returns a manifold over R\mathbb{R}:

In [4]:

Other base fields must be specified with the optional keyword field, like

M = Manifold(2, 'M', field='complex')

We may check that MM is a topological space:

In [5]:
M in Sets().Topological()

Actually, MM belongs to the following categories:

In [6]:
[SmoothR,DifferentiableR,ManifoldsR,TopologicalSpaces(Sets),Sets,SetsWithPartialMaps,Objects]\left[\mathbf{Smooth}_{\Bold{R}}, \mathbf{Differentiable}_{\Bold{R}}, \mathbf{Manifolds}_{\Bold{R}}, \mathbf{TopologicalSpaces}(\mathbf{Sets}), \mathbf{Sets}, \mathbf{SetsWithPartialMaps}, \mathbf{Objects}\right]

As we can see, by default, Manifold constructs a smooth manifold. If one would like to stick to the topological level, one should write

M = Manifold(2, 'M', structure='topological')

Smooth manifolds are implemented by the Python class DifferentiableManifold:

In [7]:

The type of M appears as DifferentiableManifold-with-category because it is actually a subclass of DifferentiableManifold, which is dynamically generated by SageMath's category mechanism:

In [8]:
isinstance(M, sage.manifolds.differentiable.manifold.DifferentiableManifold)

The class DifferentiableManifold inherits from TopologicalManifold:

In [9]:
isinstance(M, sage.manifolds.manifold.TopologicalManifold)

Coordinate charts

We declare a chart, along with the symbols used to denote the coordinates (here x=x0x=x^0 and y=x1y=x^1) by

In [10]:
U = M.open_subset('U') XU.<x,y> = U.chart() XU
(U,(x,y))\left(U,(x, y)\right)

Open subsets are implemented by a (dynamically generated) subclass of DifferentiableManifold, since they are manifolds in their own:

In [11]:
isinstance(U, sage.manifolds.differentiable.manifold.DifferentiableManifold)

Points on MM are created by passing their coordinates in a given chart:

In [12]:
p = U((1,2), chart=XU, name='p') print(p)
Point p on the 2-dimensional differentiable manifold M

The syntax U(...) used to create pp as an element of UU reflects the parent/element pattern employed in SageMath; indeed UU is the parent of pp:

In [13]:

Points are implemented by the class ManifoldPoint. The principal attribute of this class is a Python dictionary storing the point's coordinates in various charts:

In [14]:
{(U,(x,y)):(1,2)}\left\{\left(U,(x, y)\right) : \left(1, 2\right)\right\}

Of course, we can recover the point's coordinates by letting the chart act on the point:

In [15]:
(1,2)\left(1, 2\right)

Let us introduce a second chart on MM:

In [16]:
V = M.open_subset('V') XV.<xp,yp> = V.chart("xp:x' yp:y'") XV
(V,(x,y))\left(V,({x'}, {y'})\right)

and declare that MM is covered by only two charts, i.e. that M=UVM=U\cup V:

In [17]:
In [18]:
[(U,(x,y)),(V,(x,y))]\left[\left(U,(x, y)\right), \left(V,({x'}, {y'})\right)\right]

Transition map

We define the transition map XU \to XV on W=UVW = U\cap V as follows:

In [19]:
XU_to_XV = XU.transition_map(XV, (x/(x^2+y^2), y/(x^2+y^2)), intersection_name='W', restrictions1= x^2+y^2!=0, restrictions2= xp^2+yp^2!=0) XU_to_XV.display()
{x=xx2+y2y=yx2+y2\left\{\begin{array}{lcl} {x'} & = & \frac{x}{x^{2} + y^{2}} \\ {y'} & = & \frac{y}{x^{2} + y^{2}} \end{array}\right.

The value of the argument restrictions1 means that W=U{S}W = U\setminus \{S\}, where SS is the point of coordinates (x,y)=(0,0)(x,y)=(0,0), while the value of restrictions2 means that W=V{N}W = V\setminus \{N\}, where NN is the point of coordinates (x,y)=(0,0)(x',y')=(0,0). Since M=UVM=U\cup V, we have then

The transition map XV \to XU is obtained by computing the inverse of the one defined above:

In [20]:
{x=xx2+y2y=yx2+y2\left\{\begin{array}{lcl} x & = & \frac{{x'}}{{x'}^{2} + {y'}^{2}} \\ y & = & \frac{{y'}}{{x'}^{2} + {y'}^{2}} \end{array}\right.

At this stage, the smooth manifold MM is fully specified, being covered by one atlas with all transition maps specified. One may have recognized that MM is nothing but the 2-dimensional sphere: M=S2, M = \mathbb{S}^2 , with XU (resp. XV) being the chart of \defin{stereographic coordinates}\index{stereographic!coordinates} from the North pole NN (resp. the South pole SS).

Since the transition maps have been defined, we can ask for the coordinates (x,y)(x',y') of the point pp:

In [21]:
(15,25)\left(\frac{1}{5}, \frac{2}{5}\right)

This operation has updated the dictionary _coordinates:

In [22]:
{(U,(x,y)):(1,2),(V,(x,y)):(15,25)}\left\{\left(U,(x, y)\right) : \left(1, 2\right), \left(V,({x'}, {y'})\right) : \left(\frac{1}{5}, \frac{2}{5}\right)\right\}

Smooth maps

As a example of smooth map, let us consider the canonical embedding of M=S2M=\mathbb{S}^2 into R3\mathbb{R}^3. We need first to introduce R3\mathbb{R}^3 as a 3-dimensional smooth manifold, endowed with a single chart:

In [23]:
R3 = Manifold(3, 'R^3', r'\mathbb{R}^3') XR3.<X,Y,Z> = R3.chart() XR3
(R3,(X,Y,Z))\left(\mathbb{R}^3,(X, Y, Z)\right)

The embedding Φ:S2R3\Phi: \mathbb{S}^2 \to \mathbb{R}^3 is then defined in terms of its coordinate expression in the two charts covering M=S2M=\mathbb{S}^2:

In [24]:
Phi = M.diff_map(R3, {(XU, XR3): [2*x/(1+x^2+y^2), 2*y/(1+x^2+y^2), (x^2+y^2-1)/(1+x^2+y^2)], (XV, XR3): [2*xp/(1+xp^2+yp^2), 2*yp/(1+xp^2+yp^2), (1-xp^2-yp^2)/(1+xp^2+yp^2)]}, name='Phi', latex_name=r'\Phi') Phi.display()

We may use Φ\Phi for graphical purposes, for instance to display the grids of the stereographic charts XU (in red) and XV (in green), with the point pp atop:

In [25]:
graph = XU.plot(chart=XR3, mapping=Phi, number_values=25, label_axes=False) + \ XV.plot(chart=XR3, mapping=Phi, number_values=25, color='green', label_axes=False) + \ p.plot(chart=XR3, mapping=Phi, label_offset=0.05) show(graph, viewer='threejs', online=True)

Scalar fields

Let us define a scalar field, i.e. a smooth map MRM\to \mathbb{R}:

In [26]:
f = M.scalar_field({XU: 1/(1+x^2+y^2), XV: (xp^2+yp^2)/(1+xp^2+yp^2)}, name='f') f.display()

Scalar fields are implemented by the class DiffScalarField and the function chart representations are stored in the attribute _express of this class, which is a Python dictionary whose keys are the various charts defined on MM:

In [27]:
{(U,(x,y)):1x2+y2+1,(V,(x,y)):x2+y2x2+y2+1}\left\{\left(U,(x, y)\right) : \frac{1}{x^{2} + y^{2} + 1}, \left(V,({x'}, {y'})\right) : \frac{{x'}^{2} + {y'}^{2}}{{x'}^{2} + {y'}^{2} + 1}\right\}

One may wonder about the compatibility of the two coordinate expressions provided in the definition of ff. Actually, to enforce the compatibility, it is possible to declare the scalar field in a single chart, XU say, and then to obtain its expression in chart XV by analytic continuation from the expression in W=UVW=U\cap V, where both expressions are known, thanks to the transition map XV \to XU:

In [28]:
f0 = M.scalar_field({XU: 1/(1+x^2+y^2)}) f0.add_expr_by_continuation(XV, U.intersection(V)) f == f0

The representation of the scalar field in a given chart, i.e. the public access to the directory _express, is obtained via the method coord_function():

In [29]:
fU = f.coord_function(XU) fU.display()
(x,y)1x2+y2+1\left(x, y\right) \mapsto \frac{1}{x^{2} + y^{2} + 1}
In [30]:
fV = f.coord_function(XV) fV.display()
(x,y)x2+y2x2+y2+1\left({x'}, {y'}\right) \mapsto \frac{{x'}^{2} + {y'}^{2}}{{x'}^{2} + {y'}^{2} + 1}

Both fU and fV are instances of the class ChartFunction:

In [31]:
isinstance(fU, sage.manifolds.chart_func.ChartFunction)

Mathematically, chart functions are real-valued functions on the codomain of the considered chart. They map coordinates to elements of the base field (here R\mathbb{R}):

In [32]:
In [33]:

while scalar fields maps manifold points to R\mathbb{R}:

In [34]:

Internally, each chart function stores coordinate expressions with respect to various computational engines:

  • SageMath symbolic engine, based on the Pynac backend, with Maxima used for some simplifications or computation of integrals;
  • SymPy (Python library for symbolic mathematics);
  • in the future, more symbolic engines (e.g. Giac) or numerical ones will be implemented

The coordinate expressions are stored in the dictionary _express, whose keys are strings identifying the computational engines. By default only SageMath symbolic expressions, i.e. expressions pertaining to the so-called Symbolic Ring (SR), are stored:

In [35]:
{SR:1x2+y2+1}\left\{\verb|SR| : \frac{1}{x^{2} + y^{2} + 1}\right\}

The public access to the dictionary _express is performed via the method expr():

In [36]:
1x2+y2+1\frac{1}{x^{2} + y^{2} + 1}
In [37]:

Actually, fU.expr() is a shortcut for fU.expr('SR') since SR is the default symbolic engine. Note that the class Expression is that devoted to SageMath symbolic expressions. The method expr() can also be invoked to get the expression in another symbolic engine, for instance SymPy:

In [38]:
In [39]:

This operation has updated the dictionary _express:

In [40]:
{SR:1x2+y2+1,sympy:1/(x**2x+xy**2x+x1)}\left\{\verb|SR| : \frac{1}{x^{2} + y^{2} + 1}, \verb|sympy| : \verb|1/(x**2|\phantom{\verb!x!}\verb|+|\phantom{\verb!x!}\verb|y**2|\phantom{\verb!x!}\verb|+|\phantom{\verb!x!}\verb|1)|\right\}

The default calculus engine for chart functions of chart XU can changed thanks to the method set_calculus_method():

In [41]:
XU.set_calculus_method('sympy') fU.expr()

We can have better (i.e. LaTeX) rendering of SymPy objects with

from sympy import init_printing

We don't use it here to make clear the distinction between SR objects and SymPy ones.

Reverting to SageMath's symbolic engine:

In [42]:
XU.set_calculus_method('SR') fU.expr()
1x2+y2+1\frac{1}{x^{2} + y^{2} + 1}

Symbolic expressions can be accessed directly from the scalar field, f.expr(XU) being a shortcut for f.coord_function(XU).expr():

In [43]:
1x2+y2+1\frac{1}{x^{2} + y^{2} + 1}
In [44]:
x2+y2x2+y2+1\frac{{x'}^{2} + {y'}^{2}}{{x'}^{2} + {y'}^{2} + 1}

Algebra of scalar fields

The commutative algebra of scalar fields on MM is

In [45]:
CM = M.scalar_field_algebra() CM
In [46]:

As for the manifold classes, the actual Python class implementing C(M)C^\infty(M) is inherited from DiffScalarFieldAlgebra via the category mechanism, hence it bares the name DiffScalarFieldAlgebra-with-category:

In [47]:

The class DiffScalarFieldAlgebra-with-category is dynamically generated as a subclass of DiffScalarFieldAlgebra with extra functionalities, like for instance the method is_commutative():

In [48]:

Let us have a look at the code of this method; it suffices to use the double question mark:

In [49]:

We see from the File field that the code belongs to the category part of SageMath, not to the manifold part, where the class DiffScalarFieldAlgebra is defined.

Regarding the scalar field ff introduced above, we have of course

In [49]:
f in CM

Actually, in Sage language, CM=C(M)C^\infty(M) is the parent of f:

In [50]:
f.parent() is CM

The zero element of the algebra C(M)C^\infty(M) is

In [51]:

while its unit element is

In [52]:

Implementation of algebraic operations on scalar fields

Let us consider some operation in the algebra C(M)C^\infty(M):

In [53]:
h = f + 2* h.display()
In [54]:

We are going to investigate how the above addition is performed. For the Python interpreter h = f + 2* is equivalent to h = f.__add__(2*, i.e. the + operator amounts to calling the method __add__() on f. To have a look at the source code of this method, we use the double question mark:

In [56]:

We see that the method __add__() is implemented at the level of the class Element from which DiffScalarField inherits, via CommutativeAlgebraElement. In the present case, left = f and right = 2* have the same parent, namely the algebra CM, so that the actual result is computed in the line

return (<Element>left)._add_(right)

This invokes the method _add_() (note the single underscore on each side of add). This operator is implemented at the level of ScalarField, as it can be checked from the source code:

In [57]:

This reflects a general strategy in SageMath: the arithmetic Python operators __add__, __sub__, etc. are implemented at the top-level class Element, while the specific element subclasses, like ScalarField here, implement single-underscore methods _add_, _sub_, etc., which perform the actual computation when both operands have the same parent.

Looking at the code, we notice that the first step is to search for the charts in which both operands of the addition operator have a coordinate expression. This is performed by the method common_charts(); in the current case, we get the two stereographic charts defined on MM:

In [55]:
[(U,(x,y)),(V,(x,y))]\left[\left(U,(x, y)\right), \left(V,({x'}, {y'})\right)\right]

In general, common_charts() returns the charts for which both operands have already a known coordinate expression or for which a coordinate expression can be computed by a known transition map, as we can see on the source code:

In [59]:

Once the list of charts in which both operands have a coordinate expression, the addition is performed at the chart function level. The code for the addition of chart functions defined on the same chart is (recall that fU is the chart function representing ff in chart XU):

In [60]:

We notice that the addition is performed on the symbolic expression with respect to the symbolic engine currently at work (SageMath/Pynac, SymPy,...), as returned by the method expr(). Let us recall that the user can change the symbolic engine at any time by means of the method set_calculus_method(), applied either to a chart or to an open subset (possibly M itself). Besides, we notice in the code above that the result of the symbolic addition is automatically simplified, by means of the method _simplify. It invokes a chain of simplification functions, which depends on the symbolic engine (See here for details).

Let us now discuss the second case in the __add__ method of Element, namely the case for which the parents of both operands are different. This case is treated via SageMath coercion model, which allows one to deal with additions like

In [56]:
h1 = f + 2 h1.display()

A priori, f + 2 is not a well defined operation, since the integer 22 does not belong to the algebra C(M)C^\infty(M). However SageMath manages to treat it because 22 can be coerced (i.e. automatically and unambigously converted) via CM(2) into a element of C(M)C^\infty(M), namely the constant scalar field whose value is 22:

In [57]:

This happens because there exists a coercion map from the parent of 22, namely the ring of integers Z\mathbb{Z} (denoted ZZ in SageMath), to C(M)C^\infty(M):

In [58]:
In [59]:
In [ ]: