This notebook demonstrates some differential geometry capabilities of SageMath on the example of the 2-dimensional sphere. The corresponding tools have been developed within the SageManifolds project.
NB: a version of SageMath at least equal to 9.3 is required to run this notebook:
version()
'SageMath version 10.3, Release Date: 2024-03-19'
First we set up the notebook to display math formulas using LaTeX formatting:
%display latex
The 2-sphere, with predefined charts and embedding in the Euclidean 3-space, can be obtained directly from SageMath's manifold catalog:
S2 = manifolds.Sphere(2)
S2
print(S2)
2-sphere S^2 of radius 1 smoothly embedded in the Euclidean space E^3
S2.spherical_coordinates()
S2.metric().display()
For the purpose of introducing generic smooth manifolds in SageMath, we shall not use the above predefined object. Instead we shall construct $\mathbb{S}^2$ from scratch, by invoking the generic function Manifold
:
S2 = Manifold(2, 'S^2', latex_name=r'\mathbb{S}^2', start_index=1)
The first argument, 2
, is the dimension of the manifold, while the second argument is the symbol used to label the manifold.
The argument start_index
sets the index range to be used on the manifold for labelling components w.r.t. a basis or a frame: start_index=1
corresponds to $\{1,2\}$; the default value is start_index=0
and yields $\{0,1\}$.
The function Manifold
has actually many options, which are displayed via the command Manifold?
:
# Manifold?
By default Manifold
constructs a smooth manifold over $\mathbb{R}$:
print(S2)
2-dimensional differentiable manifold S^2
S2
$\mathbb{S}^2$ is in the category of smooth manifolds over $\mathbb{R}$:
S2.category()
print(S2.category())
Category of smooth manifolds over Real Field with 53 bits of precision
At the moment, the real field $\mathbb{R}$ is modeled by 53-bit floating-point approximations, but this plays no role in the manifold implementation:
print(S2.base_field())
Real Field with 53 bits of precision
S2.base_field() is RR
The function Manifold
generates a manifold with no-predefined coordinate chart, so that the manifold (user) atlas is empty:
S2.atlas()
Let us introduce some charts. At least two charts are necessary to cover the sphere. Let us choose the charts associated with the stereographic projections to the equatorial plane from the North pole and the South pole respectively. We first introduce the open subsets covered by these two charts: $$ U := \mathbb{S}^2\setminus\{N\}, $$ $$ V := \mathbb{S}^2\setminus\{S\}, $$ where $N$ is a point of $\mathbb{S}^2$, which we shall call the North pole, and $S$ is the point of $U$ of stereographic coordinates $(0,0)$, which we call the South pole:
To find the method to create an open subset, we type U = S2.<TAB>
to get the list of possible methods by autocompletion:
#U = S2.
U = S2.open_subset('U')
print(U)
Open subset U of the 2-dimensional differentiable manifold S^2
V = S2.open_subset('V')
print(V)
Open subset V of the 2-dimensional differentiable manifold S^2
As an open subset of a smooth manifold, $U$ is itself a smooth manifold:
print(U.category())
Join of Category of subobjects of sets and Category of smooth manifolds over Real Field with 53 bits of precision
We declare that $\mathbb{S}^2 = U \cup V$:
S2.declare_union(U, V)
The stereographic chart on $U$ is constructed from the stereographic projection from the North pole onto the equatorial plane: in the Wikipedia figure below, the stereographic coordinates $(x,y)$ of the point $P\in U$ are the Cartesian coordinates of the point $P'$ in the equatorial plane.
We call this chart stereoN
and construct it via the method chart
:
stereoN.<x,y> = U.chart()
The syntax .<x,y>
in the left-hand side implies that the Python names x
and y
are added to the global namespace, to access to the two coordinates of the chart as symbolic variables. This allows one to refer subsequently to the coordinates by these names. Besides, in the present case, the function chart()
has no argument, which implies that the coordinate symbols will be x
and y
(i.e. exactly the characters appearing in the <...>
operator) and that each coordinate range is $(-\infty,+\infty)$. As we will see below, for other cases, an argument must be passed to chart()
to specify each coordinate symbol and range, as well as some specific LaTeX symbol.
Note: the notation .<x,y>
is not standard Python syntax, but a "SageMath enhanced" syntax.
Actually the SageMath kernel preparses the cell entries before sending them to the Python interpreter. The outcome of the preparser is shown by the function preparse
. In the present case:
print(preparse("stereoN.<x,y> = U.chart()"))
stereoN = U.chart(names=('x', 'y',)); (x, y,) = stereoN._first_ngens(2)
Another example of preparsing:
preparse("2^3")
The chart created by the above command:
stereoN
print(stereoN)
Chart (U, (x, y))
stereoN.coord_range()
The coordinates can be accessed individually, either by means of their indices in the chart ( following the convention start_index=1
set in the manifold's definition) or by their names as Python variables:
stereoN[1]
y is stereoN[2]
The coordinates are SageMath symbolic expressions:
type(y)
y.parent()
We introduce on $V$ the coordinates $(x',y')$ corresponding to the stereographic projection from the South pole:
stereoS.<xp,yp> = V.chart("xp:x' yp:y'")
In this case, the string argument passed to chart
stipulates that the text-only names of the coordinates are xp and yp (same as the Python variables names defined within the <...>
operator in the left-hand side), while their LaTeX names are $x'$ and $y'$.
stereoS
At this stage, the user's atlas on the manifold is made of two charts:
S2.atlas()
To complete the construction of the manifold structure, we have
to specify the transition map between the charts stereoN
= $(U,(x,y))$ and stereoS
= $(V,(x',y'))$; it is given by standard inversion formulas:
stereoN_to_S = stereoN.transition_map(stereoS,
(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)
stereoN_to_S.display()
In the above declaration, 'W' is the name given to the chart-overlap subset: $W := U\cap V$, the condition $x^2+y^2 \not=0$ defines $W$ as a subset of $U$, and the condition $x'^2+y'^2\not=0$ defines $W$ as a subset of $V$.
The inverse coordinate transformation is computed by means of the method inverse()
:
stereoS_to_N = stereoN_to_S.inverse()
stereoS_to_N.display()
In the present case, the situation is of course perfectly symmetric regarding the coordinates $(x,y)$ and $(x',y')$.
At this stage, the user's atlas has four charts:
S2.atlas()
Let us store $W = U\cap V$ into a Python variable for future use:
W = U.intersection(V)
Similarly we store the charts $(W,(x,y))$ (the restriction of $(U,(x,y))$ to $W$) and $(W,(x',y'))$ (the restriction of $(V,(x',y'))$ to $W$) into Python variables:
stereoN_W = stereoN.restrict(W)
stereoN_W
stereoN_W is S2.atlas()[2]
stereoS_W = stereoS.restrict(W)
stereoS_W
Coordinate charts are endoved with a method plot
. For instance,
we may plot the chart $(W, (x',y'))$ in terms of itself, as a grid:
stereoS_W.plot()
More interestingly, let us plot the stereographic chart $(x',y')$ in terms of the stereographic chart $(x,y)$ on the domain $W$ where both systems overlap. We split the plot in four parts to avoid the singularity at $(x',y')=(0,0)$ and ask for the coordinate lines along which $x'$ (resp. $y'$) varies to be colored in purple (resp. cyan):
graph = (stereoS_W.plot(stereoN, ranges={xp:[-6,-0.02], yp:[-6,-0.02]},
color={xp: 'purple', yp: 'cyan'})
+ stereoS_W.plot(stereoN, ranges={xp:[-6,-0.02], yp:[0.02,6]},
color={xp: 'purple', yp: 'cyan'})
+ stereoS_W.plot(stereoN, ranges={xp:[0.02,6], yp:[-6,-0.02]},
color={xp: 'purple', yp: 'cyan'})
+ stereoS_W.plot(stereoN, ranges={xp:[0.02,6], yp:[0.02,6]},
color={xp: 'purple', yp: 'cyan'}))
graph.show(xmin=-1.5, xmax=1.5, ymin=-1.5, ymax=1.5)
The standard spherical coordinates $(\theta,\phi)$ are defined on the open domain $A\subset W \subset \mathbb{S}^2$ that is the complement of the "origin meridian"; since the latter is the half-circle defined by $y=0$ and $x\geq 0$, we declare:
A = W.open_subset('A', coord_def={stereoN_W: (y!=0, x<0),
stereoS_W: (yp!=0, xp<0)})
print(A)
Open subset A of the 2-dimensional differentiable manifold S^2
The restriction of the stereographic chart from the North pole to $A$ is
stereoN_A = stereoN_W.restrict(A)
stereoN_A
We then declare the chart $(A,(\theta,\phi))$ by specifying the intervals $(0,\pi)$ and $(0,2\pi)$ spanned by respectively $\theta$ and $\phi$:
spher.<th,ph> = A.chart(r'th:(0,pi):\theta ph:(0,2*pi):\phi')
spher
spher.coord_range()
The specification of the spherical coordinates is completed by providing the transition map with the stereographic chart $(A,(x,y))$:
spher_to_stereoN = spher.transition_map(stereoN_A,
(sin(th)*cos(ph)/(1-cos(th)),
sin(th)*sin(ph)/(1-cos(th))))
spher_to_stereoN.display()
We also provide the inverse transition map:
spher_to_stereoN.set_inverse(2*atan(1/sqrt(x^2+y^2)), atan2(-y,-x)+pi)
Check of the inverse coordinate transformation: th == 2*arctan(sqrt(-cos(th) + 1)/sqrt(cos(th) + 1)) **failed** ph == pi + arctan2(sin(ph)*sin(th)/(cos(th) - 1), cos(ph)*sin(th)/(cos(th) - 1)) **failed** x == x *passed* y == y *passed* NB: a failed report can reflect a mere lack of simplification.
The check is passed, modulo some lack of trigonometric simplifications in the first two lines.
spher_to_stereoN.inverse().display()
The transition map $(A,(\theta,\phi))\rightarrow (A,(x',y'))$ is obtained by combining the transition maps $(A,(\theta,\phi))\rightarrow (A,(x,y))$ and $(A,(x,y))\rightarrow (A,(x',y'))$ via the operator *
:
stereoN_to_S_A = stereoN_to_S.restrict(A)
spher_to_stereoS = stereoN_to_S_A * spher_to_stereoN
spher_to_stereoS.display()
Similarly, the transition map $(A,(x',y'))\rightarrow (A,(\theta,\phi))$ is obtained by combining the transition maps $(A,(x',y'))\rightarrow (A,(x,y))$ and $(A,(x,y))\rightarrow (A,(\theta,\phi))$:
stereoS_to_N_A = stereoN_to_S.inverse().restrict(A)
stereoS_to_spher = spher_to_stereoN.inverse() * stereoS_to_N_A
stereoS_to_spher.display()
The user atlas of $\mathbb{S}^2$ is now
S2.atlas()
Let us draw the grid of spherical coordinates $(\theta,\phi)$ in terms of stereographic coordinates from the North pole $(x,y)$:
spher.plot(stereoN, number_values=15, ranges={th: (pi/8,pi)})
To create a point on $\mathbb{S}^2$, we use SageMath's *parent / element* syntax, i.e. the call operator S2(...)
acting on the parent S2
, with the point's coordinates in some chart as argument.
For instance, we declare the North pole (resp. the South pole) as the point of coordinates $(0,0)$ in the chart $(V,(x',y'))$ (resp. in the chart $(U,(x,y))$):
N = S2((0,0), chart=stereoS, name='N')
print(N)
Point N on the 2-dimensional differentiable manifold S^2
S = S2((0,0), chart=stereoN, name='S')
print(S)
Point S on the 2-dimensional differentiable manifold S^2
N.parent()
S.parent()
We have of course
N in S2
N in U
N in V
N in A
Let us introduce some point $p$ of stereographic coordinates $(x,y) = (1,2)$:
p = S2((1,2), chart=stereoN, name='p')
$p$ lies in the open subset $A$:
p in A
By definition, a chart maps points to pairs of real numbers (the point's coordinates):
stereoN(p) # by definition of p
stereoS(p)
spher(p)
stereoS(N)
#stereoN(N) ## returns an error
Let us first declare $\mathbb{R}^3$ as the 3-dimensional Euclidean space, denoting the Cartesian coordinates by $(X,Y,Z)$:
R3.<X,Y,Z> = EuclideanSpace(name='R^3', latex_name=r'\mathbb{R}^3', metric_name='h')
cartesian = R3.cartesian_coordinates()
cartesian
As an Euclidean space, R3
is considered by Sage as a smooth manifold:
print(R3.category())
Join of Category of smooth manifolds over Real Field with 53 bits of precision and Category of connected manifolds over Real Field with 53 bits of precision and Category of complete metric spaces
The embedding $\Phi: \mathbb{S}^2 \longmapsto \mathbb{R}^3$ is then defined via the method diff_map
by providing the standard formulas relating the stereographic coordinates to the ambient Cartesian ones when considering the stereographic projection from the point $(0,0,1)$ (North pole) or $(0, 0, -1)$ (South pole) to the equatorial plane $Z=0$:
Phi = S2.diff_map(R3, {(stereoN, cartesian):
[2*x/(1+x^2+y^2), 2*y/(1+x^2+y^2),
(x^2+y^2-1)/(1+x^2+y^2)],
(stereoS, cartesian):
[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()
Phi.parent()
print(Phi.parent())
Set of Morphisms from 2-dimensional differentiable manifold S^2 to Euclidean space R^3 in Category of smooth manifolds over Real Field with 53 bits of precision
Phi.parent() is Hom(S2, R3)
$\Phi$ maps points of $\mathbb{S}^2$ to points of $\mathbb{R}^3$:
N1 = Phi(N)
print(N1)
N1
Point Phi(N) on the Euclidean space R^3
cartesian(N1)
S1 = Phi(S)
print(S1)
S1
Point Phi(S) on the Euclidean space R^3
cartesian(S1)
p1 = Phi(p)
print(p1)
p1
Point Phi(p) on the Euclidean space R^3
cartesian(p1)
$\Phi$ has been defined in terms of the stereographic charts $(U,(x,y))$ and $(V,(x',y'))$, but we may ask its expression in terms of spherical coordinates. This triggers a computation involving the transition map $(A,(x,y))\rightarrow (A,(\theta,\phi))$:
Phi.display(stereoN_A, cartesian)
Phi.display(spher, cartesian)
Let us use $\Phi$ to draw the grid of spherical coordinates $(\theta,\phi)$ in terms of the Cartesian coordinates $(X,Y,Z)$ of $\mathbb{R}^3$:
graph_spher = spher.plot(chart=cartesian, mapping=Phi, number_values=11,
color='blue', label_axes=False)
graph_spher
We may also use the embedding $\Phi$ to display the stereographic coordinate grid in terms of the Cartesian coordinates in $\mathbb{R}^3$. First for the stereographic coordinates from the North pole:
graph = stereoN.plot(chart=cartesian, mapping=Phi, number_values=25,
label_axes=False)
graph
and then have a view with the stereographic coordinates from the South pole superposed (in green):
graph += stereoS.plot(chart=cartesian, mapping=Phi, number_values=25,
color='green', label_axes=False)
graph
We may add the points $N$, $S$ and $p$ to the graphic, thanks to the method plot
of points:
graph += N.plot(chart=cartesian, mapping=Phi, color='red',
label_offset=0.05)
graph += S.plot(chart=cartesian, mapping=Phi, color='green',
label_offset=0.05)
graph += p.plot(chart=cartesian, mapping=Phi, color='blue',
label_offset=0.05)
graph
The tangent space to the manifold $\mathbb{S}^2$ at the point $p$ is
Tp = S2.tangent_space(p)
print(Tp)
Tp
Tangent space at Point p on the 2-dimensional differentiable manifold S^2
$T_p \mathbb{S}^2$ is a vector space over $\mathbb{R}$ (represented here by Sage's symbolic ring SR):
print(Tp.category())
Category of finite dimensional vector spaces over Symbolic Ring
Its dimension equals the manifold's dimension:
dim(Tp)
dim(Tp) == dim(S2)
The tangent space at $p$ is the fiber over $p$ of the tangent bundle $T\mathbb{S}^2$:
Tp is S2.tangent_bundle().fiber(p)
The vector space $T_p \mathbb{S}^2$ is endowed with bases inherited from the coordinate frames defined around $p$:
Tp.bases()
On the contrary, since $(V,(x',y'))$ is the only chart defined so far around the point $N$, we have a unique predefined basis in $T_N \mathbb{S}^2$:
T_N = S2.tangent_space(N)
T_N.bases()
To shorten some writings, there is the concept of default basis:
Tp.default_basis()
An element of $T_p\mathbb{S}^2$ is constructed via SageMath's parent/element syntax, i.e. via the call method of the parent:
v = Tp((-2, 3), name='v')
print(v)
Tangent vector v at Point p on the 2-dimensional differentiable manifold S^2
Equivalently, one can use the method tangent_vector
of manifolds:
v == S2.tangent_vector(p, -2, 3, name='v')
One has of course:
v in Tp
v.parent()
The vector $v$ expanded in the default basis of $T_p \mathbb{S}^2$:
v.display()
Expansion in other bases:
v.display(Tp.bases()[1])
v.display(Tp.bases()[2])
Tangent vectors are endowed with a method plot
:
graph += v.plot(chart=cartesian, mapping=Phi, scale=0.2, width=0.5)
graph
The differential of the map $\Phi$ at the point $p$ is
dPhi_p = Phi.differential(p)
print(dPhi_p)
dPhi_p
Generic morphism: From: Tangent space at Point p on the 2-dimensional differentiable manifold S^2 To: Tangent space at Point Phi(p) on the Euclidean space R^3
dPhi_p.domain()
dPhi_p.codomain()
dPhi_p.parent()
The image by $\mathrm{d}\Phi_p$ of the vector $v\in T_p\mathbb{S}^2$ introduced above is
dPhi_p(v)
print(dPhi_p(v))
Vector dPhi_p(v) at Point Phi(p) on the Euclidean space R^3
dPhi_p(v) in R3.tangent_space(Phi(p))
dPhi_p(v).display()
The set $C^\infty(\mathbb{S}^2)$ of all smooth functions $\mathbb{S}^2\rightarrow \mathbb{R}$ has naturally the structure of a commutative algebra over $\mathbb{R}$. $C^\infty(\mathbb{S}^2)$ is therefore returned by the method scalar_field_algebra()
of manifolds:
CS = S2.scalar_field_algebra()
CS
Since the algebra internal product is the pointwise multiplication, it is clearly commutative, so that $C^\infty(\mathbb{S}^2)$ belongs to Sage's category of commutative algebras:
print(CS.category())
Join of Category of commutative algebras over Symbolic Ring and Category of homsets of topological spaces
The base ring of the algebra $C^\infty(\mathbb{S}^2)$ is the field $\mathbb{R}$, which is represented here by Sage's Symbolic Ring (SR):
CS.base_ring()
Elements of $C^\infty(\mathbb{S}^2)$ are of course (smooth) scalar fields:
print(CS.an_element())
Scalar field on the 2-dimensional differentiable manifold S^2
This example element is the constant scalar field that takes the value 2:
CS.an_element().display()
A specific element is the zero one:
f = CS.zero()
print(f)
Scalar field zero on the 2-dimensional differentiable manifold S^2
Scalar fields map points of $\mathbb{S}^2$ to real numbers:
f(N), f(S), f(p)
f.display()
Another specific element is the algebra unit element, i.e. the constant scalar field 1:
f = CS.one()
print(f)
Scalar field 1 on the 2-dimensional differentiable manifold S^2
f(N), f(S), f(p)
A generic scalar field is defined by its coordinate expression in some chart(s); for instance:
f = S2.scalar_field({stereoN: 1/(1+x^2+y^2)}, name='f')
f.display()
We see that Sage has used the transition map between the two stereographic charts on $W$ to express $f$ in terms of the coordinates $(x',y')$ on $W$. Let us this expression to extend $f$ to the whole of $V$:
f.add_expr_by_continuation(stereoS, W)
Then $f$ is well defined in all $\mathbb{S}^2 = U \cup V$:
f.display()
f(N)
f.parent()
Scalar fields map the manifold's points to real numbers:
f(N)
f(S)
f(p)
We may define the restrictions of $f$ to the open subsets $U$ and $V$:
fU = f.restrict(U)
fU.display()
fV = f.restrict(V)
fV.display()
fU(p), fU(S)
fU.parent()
fV.parent()
CU = U.scalar_field_algebra()
fU.parent() is CU
A scalar field on $\mathbb{S}^2$ can be coerced to a scalar field on $U$, the coercion being simply the restriction:
CU.has_coerce_map_from(CS)
fU == CU(f)
The arithmetic of scalar fields (operations in the algebra $C^\infty(\mathbb{S}^2)$):
g = f*f - 2*f
g.set_name('g')
g.display()
The set $\mathfrak{X}(\mathbb{S}^2)$ of all smooth vector fields on $\mathbb{S}^2$ is a module over the algebra $C^\infty(\mathbb{S}^2)$. It is obtained by the method vector_field_module()
:
XS = S2.vector_field_module()
XS
print(XS)
Module X(S^2) of vector fields on the 2-dimensional differentiable manifold S^2
XS.base_ring()
XS.category()
$\mathfrak{X}(\mathbb{S}^2)$ is not a free module:
isinstance(XS, FiniteRankFreeModule)
because $\mathbb{S}^2$ is not a parallelizable manifold:
S2.is_manifestly_parallelizable()
On the contrary, the set $\mathfrak{X}(U)$ of smooth vector fields on $U$ is a free module:
XU = U.vector_field_module()
isinstance(XU, FiniteRankFreeModule)
because $U$ is parallelizable:
U.is_manifestly_parallelizable()
Due to the introduction of the stereographic coordinates $(x,y)$ on $U$, a basis has already been defined on the free module $\mathfrak{X}(U)$, namely the coordinate basis $(\partial/\partial x, \partial/\partial y)$:
XU.print_bases()
Bases defined on the Free module X(U) of vector fields on the Open subset U of the 2-dimensional differentiable manifold S^2: - (U, (∂/∂x,∂/∂y)) (default basis)
eU = XU.default_basis()
eU
Similarly
XV = V.vector_field_module()
eV = XV.default_basis()
eV
From the point of view of the open set $U$, eU
is also the default vector frame:
eU is U.default_frame()
Similarly:
eV is V.default_frame()
eU
is also the default vector frame on $\mathbb{S}^2$ (although not defined on the whole $\mathbb{S}^2$), for it is the first vector frame defined on an open subset of $\mathbb{S}^2$:
eU is S2.default_frame()
S2.frames()
Let us introduce a vector field on $\mathbb{S}^2$ by providing its components in the
frame eU
:
v = S2.vector_field(1, -2, frame=eU, name='v')
v.display(eU)
v.parent()
On $W$, we can express $v$ in terms of the $(x',y')$ coordinates:
v.restrict(W).display(stereoS.restrict(W).frame(), stereoS.restrict(W))
We extend the definition of $v$ to $V$ thanks to the above expression:
v.add_comp_by_continuation(eV, W, chart=stereoS)
v.display(eV)
At this stage, the vector field $v$ is defined on the whole manifold $\mathbb{S}^2$: it has expressions in each of the two frames eU
and eV
, which cover $\mathbb{S}^2$.
According to the hairy ball theorem, $v$ has to vanish somewhere. This occurs at the North pole:
vN = v.at(N)
print(vN)
Tangent vector v at Point N on the 2-dimensional differentiable manifold S^2
vN.display()
$v|_N$ is the zero vector of the tangent vector space $T_N\mathbb{S}^2$:
vN.parent()
vN.parent() is S2.tangent_space(N)
vN == S2.tangent_space(N).zero()
On the contrary, $v$ is non-zero at the South pole:
vS = v.at(S)
print(v)
Vector field v on the 2-dimensional differentiable manifold S^2
vS.display()
Let us plot the vector field $v$ is terms of the stereographic chart $(U,(x,y))$, with the South pole $S$ superposed:
v.plot(chart=stereoN, chart_domain=stereoN, max_range=4,
number_values=5, scale=0.5, aspect_ratio=1) \
+ S.plot(stereoN, size=30, label_offset=0.2)
The vector field appears homogeneous because its components w.r.t. the frame $\left(\frac{\partial}{\partial x}, \frac{\partial}{\partial y}\right)$ are constant:
v.display(stereoN.frame())
On the contrary, once drawn in terms of the stereographic chart $(V, (x',y'))$, $v$ does no longer appears homogeneous:
v.plot(chart=stereoS, chart_domain=stereoS, max_range=4, scale=0.02,
aspect_ratio=1) \
+ N.plot(chart=stereoS, size=30, label_offset=0.2)
Finally, a 3D view of the vector field $v$ is obtained via the embedding $\Phi$:
graph_v = v.plot(chart=cartesian, mapping=Phi, chart_domain=spher,
number_values=11, scale=0.2)
graph_spher + graph_v
Similarly, let us draw the first vector field of the stereographic frame from the North pole, namely $\frac{\partial}{\partial x}$:
stereoN.frame()
ex = stereoN.frame()[1]
ex
graph_ex = ex.plot(chart=cartesian, mapping=Phi, chart_domain=spher,
number_values=11, scale=0.4, width=1,
label_axes=False)
graph_spher + graph_ex
For the second vector field of the stereographic frame from the North pole, namely $\frac{\partial}{\partial y}$, we get
ey = stereoN.frame()[2]
ey
graph_ey = ey.plot(chart=cartesian, mapping=Phi, chart_domain=spher,
number_values=11, scale=0.4, width=1, color='red',
label_axes=False)
graph_spher + graph_ey
We may combine the two graphs, to get a 3D view of the vector frame associated with the stereographic coordinates from the North pole:
graph_frame = graph_spher + graph_ex + graph_ey \
+ N.plot(cartesian, mapping=Phi, label_offset=0.05, size=5) \
+ S.plot(cartesian, mapping=Phi, label_offset=0.05, size=5)
graph_frame + sphere(color='lightgrey', opacity=0.4)
The same scene rendered with Tachyon:
show(graph_frame + sphere(opacity=0.5), viewer='tachyon', figsize=10)
$v$ and $f$ are both fields defined on the whole sphere (respectively a vector field and a scalar field). By the very definition of a vector field, $v$ acts on $f$:
vf = v(f)
print(vf)
vf.display()
Scalar field v(f) on the 2-dimensional differentiable manifold S^2
Values of $v(f)$ at the North pole, at the South pole and at point $p$:
vf(N)
vf(S)
vf(p)
A 1-form on $\mathbb{S}^2$ is a field of linear forms on the tangent spaces. For instance it can be the differential of a scalar field:
f.display()
df = diff(f)
print(df)
1-form df on the 2-dimensional differentiable manifold S^2
df.display() # display w.r.t. the default frame
df.display(eV)
df.display(spher.frame())
df.display(spher.frame(), spher) # asking for the components to be shown in the spherical chart
print(df.parent())
Module Omega^1(S^2) of 1-forms on the 2-dimensional differentiable manifold S^2
df.parent()
The 1-form acting on a vector field:
print(df(v))
df(v).display()
Scalar field df(v) on the 2-dimensional differentiable manifold S^2
Let us check the identity $\mathrm{d}f(v) = v(f)$:
df(v) == v(f)
Similarly, we have $\mathcal{L}_v f = v(f)$:
f.lie_derivative(v) == v(f)
In order to define curves in $\mathbb{S}^2$, we first introduce the field of real numbers $\mathbb{R}$ as a 1-dimensional smooth manifold with a canonical coordinate chart:
R.<t> = manifolds.RealLine()
print(R)
Real number line ℝ
print(R.category())
Category of smooth connected manifolds over Real Field with 53 bits of precision
dim(R)
R.atlas()
Let us define a loxodrome of the sphere in terms of its parametric equation with respect to the chart spher = $(A,(\theta,\phi))$
c = S2.curve({spher: [2*atan(exp(-t/10)), t]}, (t, -oo, +oo), name='c')
Curves in $\mathbb{S}^2$ are considered as morphisms from the manifold $\mathbb{R}$ to the manifold $\mathbb{S}^2$:
c.parent()
c.display()
The curve $c$ can be plotted in terms of stereographic coordinates $(x,y)$:
c.plot(chart=stereoN, aspect_ratio=1)
We recover the well-known fact that the graph of a loxodrome in terms of stereographic coordinates is a logarithmic spiral.
Thanks to the embedding $\Phi$, we may also plot $c$ in terms of the Cartesian coordinates of $\mathbb{R}^3$:
graph_c = c.plot(mapping=Phi, max_range=40, plot_points=200,
thickness=2, label_axes=False)
graph_spher + graph_c
The tangent vector field (or velocity vector) to the curve $c$ is
vc = c.tangent_vector_field()
vc
$c'$ is a vector field along $\mathbb{R}$ taking its values in tangent spaces to $\mathbb{S}^2$:
print(vc)
Vector field c' along the Real number line ℝ with values on the 2-dimensional differentiable manifold S^2
The set of vector fields along $\mathbb{R}$ taking their values on $\mathbb{S}^2$ via the differential mapping $c: \mathbb{R} \rightarrow \mathbb{S}^2$ is denoted by $\mathfrak{X}(\mathbb{R},c)$; it is a module over the algebra $C^\infty(\mathbb{R})$:
vc.parent()
vc.parent().category()
vc.parent().base_ring()
A coordinate view of $c'$:
vc.display()
Let us plot the vector field $c'$ in terms of the stereographic chart $(U,(x,y))$:
show(vc.plot(chart=stereoN, number_values=30, scale=0.5, color='red') +
c.plot(chart=stereoN), aspect_ratio=1)
A 3D view of $c'$ is obtained via the embedding $\Phi$:
graph_vc = vc.plot(chart=cartesian, mapping=Phi, ranges={t: (-20, 20)},
number_values=30, scale=0.5, color='red',
label_axes=False)
graph_spher + graph_c + graph_vc
The standard metric on $\mathbb{S}^2$ is that induced by the Euclidean metric of $\mathbb{R}^3$. The latter is obtained by:
h = R3.metric()
h.display()
The metric $g$ on $\mathbb{S}^2$ is the pullback of $h$ by the embedding $\Phi$:
g = S2.metric('g')
g.set( Phi.pullback(h) )
print(g)
Riemannian metric g on the 2-dimensional differentiable manifold S^2
Note that we could have defined $g$ intrinsically, i.e. by providing its components in the two frames stereoN
and stereoS
. Instead, we have chosen to get it as the pullback of $h$, since the pullback computation is implemented in SageMath.
The metric is a symmetric tensor field of type (0,2):
print(g.parent())
Module T^(0,2)(S^2) of type-(0,2) tensors fields on the 2-dimensional differentiable manifold S^2
g.tensor_type()
g.symmetries()
symmetry: (0, 1); no antisymmetry
The expression of the metric in terms of the default frame on $\mathbb{S}^2$ (stereoN):
g.display()
We may factorize the metric components:
g.apply_map(factor, frame=eU, keep_other_components=True)
g.display()
A matrix view of the components of $g$ in the manifold's default frame:
g[:]
g[1,1]
Display in terms of the vector frame eV
= $(V, (\partial_{x'}, \partial_{y'}))$:
g.apply_map(factor, frame=eV, keep_other_components=True)
g.display(eV)
Expression of the metric tensor in terms of spherical coordinates:
g.display(spher.frame(), chart=spher)
The metric acts on vector field pairs, resulting in a scalar field:
print(g(v,v))
Scalar field g(v,v) on the 2-dimensional differentiable manifold S^2
g(v,v).parent()
g(v,v).display()
The Levi-Civita connection associated with the metric $g$:
nabla = g.connection()
print(nabla)
nabla
Levi-Civita connection nabla_g associated with the Riemannian metric g on the 2-dimensional differentiable manifold S^2
As a test, we verify that $\nabla_g$ acting on $g$ results in zero:
nabla(g).display()
The nonzero Christoffel symbols of $g$ (skipping those that can be deduced by symmetry on the last two indices) w.r.t. two charts:
g.christoffel_symbols_display(chart=stereoN)
g.christoffel_symbols_display(chart=spher)
$\nabla_g$ acting on the vector field $v$:
print(nabla(v))
Tensor field nabla_g(v) of type (1,1) on the 2-dimensional differentiable manifold S^2
nabla(v).display(stereoN.frame())
nabla(v)[:]
nabla(v)[1,2]
The Riemann tensor associated with the metric $g$:
Riem = g.riemann()
print(Riem)
Riem.display()
Tensor field Riem(g) of type (1,3) on the 2-dimensional differentiable manifold S^2
The components of the Riemann tensor in the default frame on $\mathbb{S}^2$:
Riem.display_comp()
The components in the frame associated with spherical coordinates:
Riem.display_comp(spher.frame(), chart=spher)
print(Riem.parent())
Module T^(1,3)(S^2) of type-(1,3) tensors fields on the 2-dimensional differentiable manifold S^2
Riem.symmetries()
no symmetry; antisymmetry: (2, 3)
The Riemann tensor associated with the Euclidean metric $h$ on $\mathbb{R}^3$ is identically zero:
h.riemann().display()
The Ricci tensor and the Ricci scalar:
Ric = g.ricci()
Ric.display()
print(g.inverse())
Tensor field inv_g of type (2,0) on the 2-dimensional differentiable manifold S^2
g.inverse().display()
R = g.ricci_scalar()
R.display()
Hence we recover the fact that $(\mathbb{S}^2,g)$ is a Riemannian manifold of constant positive curvature.
In dimension 2, the Riemann curvature tensor is entirely determined by the Ricci scalar $R$ according to $$ R^i_{\ \, jlk} = \frac{R}{2} \left( \delta^i_{\ \, k} g_{jl} - \delta^i_{\ \, l} g_{jk} \right)$$ Let us check this formula here, under the form $R^i_{\ \, jlk} = -R g_{j[k} \delta^i_{\ \, l]}$:
delta = S2.tangent_identity_field()
Riem == - R*(g*delta).antisymmetrize(2,3)
Similarly the relation $\mathrm{Ric} = (R/2)\; g$ must hold:
Ric == (R/2)*g
In order to introduce the volume 2-form associated with the metric $g$, we need to define an orientation on $\mathbb{S}^2$ first. We choose the orientation so that the vector frame $(\partial/\partial x', \partial/\partial y')$ of the stereographic coordinates from the South pole is right-handed. This is somewhat natural, because the triplet $(\partial/\partial x', \partial/\partial y', n)$, where $n$ is the unit outward normal to $\mathbb{S}^2$, is right-handed with respect to the standard orientation of $\mathbb{R}^3$. On the contrary the triplet $(\partial/\partial x, \partial/\partial y, n)$ formed from stereographic coordinates from the North pole is left-handed (see the above plot). Actually, we can check that $(\partial/\partial x, \partial/\partial y)$ and $(\partial/\partial x', \partial/\partial y')$ lead to two opposite orientations, because the transition map $(x, y) \mapsto (x', y')$ has a negative Jacobian determinant:
stereoN_to_S.jacobian_det()
We define the orientation via the method set_orientation()
with a list of right-handed vector frames, whose domains form an open cover of $\mathbb{S}^2$. We therefore provide eV
= $(\partial/\partial x', \partial/\partial y')$ (domain: $V$) and the "reversed" frame $(\partial/\partial y, \partial/\partial x)$ on $U$:
reU = U.vector_frame('f', (eU[2], eU[1]))
reU[1].display(eU), reU[2].display(eU)
S2.set_orientation([eV, reU])
The volume 2-form or Levi-Civita tensor associated with $g$ is then
eps = g.volume_form()
print(eps)
eps.display()
2-form eps_g on the 2-dimensional differentiable manifold S^2
Notice the minus sign in the the above expression, which reflects the fact that the default frame $(\partial/\partial x, \partial/\partial y)$ is left-handed. On the contrary, we have
eps.display(eV)
A nicer display is obtained by factorizing the components:
eps.apply_map(factor, frame=eV, keep_other_components=True)
eps.display(stereoS.frame())
The frame associated with spherical coordinates is right-handed and we recover the standard expression of the volume 2-form:
eps.display(spher.frame(), chart=spher)
The exterior derivative of the 2-form $\epsilon_g$:
print(diff(eps))
3-form deps_g on the 2-dimensional differentiable manifold S^2
Of course, since $\mathbb{S}^2$ has dimension 2, all 3-forms vanish identically:
diff(eps).display()
Up to know, all the vector frames introduced on $\mathbb{S}^2$ have been coordinate frames. Let us introduce a non-coordinate frame on the open subset $A$. To ease the manipulations, we change first the default chart and default frame on $A$ to the spherical coordinate ones:
A.default_chart()
A.default_frame()
A.set_default_chart(spher)
A.set_default_frame(spher.frame())
A.default_chart()
A.default_frame()
We introduce the new vector frame $e = \left(\frac{\partial}{\partial\theta}, \frac{1}{\sin\theta}\frac{\partial}{\partial\phi}\right)$:
spher.frame()[:]
d_dth, d_dph = spher.frame()[:]
e = A.vector_frame('e', (d_dth, 1/sin(th)*d_dph))
print(e)
e
Vector frame (A, (e_1,e_2))
(e[1].display(), e[2].display())
The new frame is an orthonormal frame for the metric $g$:
g(e[1],e[1]).expr()
g(e[1],e[2]).expr()
g(e[2],e[2]).expr()
g[e,:]
g.display(e)
eps.display(e)
It is non-holonomic, since its structure coefficients are not identically zero:
e.structure_coeff()[:]
e[2].lie_derivative(e[1]).display(e)
while we have of course
spher.frame().structure_coeff()[:]
S2.set_calculus_method('sympy')
F = 2*f
F.display()
F.expr()
type(F.expr())
Back to Sage's default:
S2.set_calculus_method('SR')
F.expr()
type(F.expr())
See the notebooks Smooth manifolds, charts and scalar fields and Smooth manifolds, vector fields and tensor fields from the lectures Symbolic tensor calculus on manifolds. Many example notebooks are provided at the SageManifolds page.
See also the series of notebooks by Andrzej Chrzeszczyk: Introduction to manifolds in SageMath, as well as the tutorial videos by Christian Bär: Manifolds in SageMath.