You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2274 lines
71 KiB
2274 lines
71 KiB
5 months ago
|
from __future__ import annotations
|
||
|
from typing import Any
|
||
|
|
||
|
from functools import reduce
|
||
|
from itertools import permutations
|
||
|
|
||
|
from sympy.combinatorics import Permutation
|
||
|
from sympy.core import (
|
||
|
Basic, Expr, Function, diff,
|
||
|
Pow, Mul, Add, Lambda, S, Tuple, Dict
|
||
|
)
|
||
|
from sympy.core.cache import cacheit
|
||
|
|
||
|
from sympy.core.symbol import Symbol, Dummy
|
||
|
from sympy.core.symbol import Str
|
||
|
from sympy.core.sympify import _sympify
|
||
|
from sympy.functions import factorial
|
||
|
from sympy.matrices import ImmutableDenseMatrix as Matrix
|
||
|
from sympy.solvers import solve
|
||
|
|
||
|
from sympy.utilities.exceptions import (sympy_deprecation_warning,
|
||
|
SymPyDeprecationWarning,
|
||
|
ignore_warnings)
|
||
|
|
||
|
|
||
|
# TODO you are a bit excessive in the use of Dummies
|
||
|
# TODO dummy point, literal field
|
||
|
# TODO too often one needs to call doit or simplify on the output, check the
|
||
|
# tests and find out why
|
||
|
from sympy.tensor.array import ImmutableDenseNDimArray
|
||
|
|
||
|
|
||
|
class Manifold(Basic):
|
||
|
"""
|
||
|
A mathematical manifold.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
A manifold is a topological space that locally resembles
|
||
|
Euclidean space near each point [1].
|
||
|
This class does not provide any means to study the topological
|
||
|
characteristics of the manifold that it represents, though.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
name : str
|
||
|
The name of the manifold.
|
||
|
|
||
|
dim : int
|
||
|
The dimension of the manifold.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom import Manifold
|
||
|
>>> m = Manifold('M', 2)
|
||
|
>>> m
|
||
|
M
|
||
|
>>> m.dim
|
||
|
2
|
||
|
|
||
|
References
|
||
|
==========
|
||
|
|
||
|
.. [1] https://en.wikipedia.org/wiki/Manifold
|
||
|
"""
|
||
|
|
||
|
def __new__(cls, name, dim, **kwargs):
|
||
|
if not isinstance(name, Str):
|
||
|
name = Str(name)
|
||
|
dim = _sympify(dim)
|
||
|
obj = super().__new__(cls, name, dim)
|
||
|
|
||
|
obj.patches = _deprecated_list(
|
||
|
"""
|
||
|
Manifold.patches is deprecated. The Manifold object is now
|
||
|
immutable. Instead use a separate list to keep track of the
|
||
|
patches.
|
||
|
""", [])
|
||
|
return obj
|
||
|
|
||
|
@property
|
||
|
def name(self):
|
||
|
return self.args[0]
|
||
|
|
||
|
@property
|
||
|
def dim(self):
|
||
|
return self.args[1]
|
||
|
|
||
|
|
||
|
class Patch(Basic):
|
||
|
"""
|
||
|
A patch on a manifold.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Coordinate patch, or patch in short, is a simply-connected open set around
|
||
|
a point in the manifold [1]. On a manifold one can have many patches that
|
||
|
do not always include the whole manifold. On these patches coordinate
|
||
|
charts can be defined that permit the parameterization of any point on the
|
||
|
patch in terms of a tuple of real numbers (the coordinates).
|
||
|
|
||
|
This class does not provide any means to study the topological
|
||
|
characteristics of the patch that it represents.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
name : str
|
||
|
The name of the patch.
|
||
|
|
||
|
manifold : Manifold
|
||
|
The manifold on which the patch is defined.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom import Manifold, Patch
|
||
|
>>> m = Manifold('M', 2)
|
||
|
>>> p = Patch('P', m)
|
||
|
>>> p
|
||
|
P
|
||
|
>>> p.dim
|
||
|
2
|
||
|
|
||
|
References
|
||
|
==========
|
||
|
|
||
|
.. [1] G. Sussman, J. Wisdom, W. Farr, Functional Differential Geometry
|
||
|
(2013)
|
||
|
|
||
|
"""
|
||
|
def __new__(cls, name, manifold, **kwargs):
|
||
|
if not isinstance(name, Str):
|
||
|
name = Str(name)
|
||
|
obj = super().__new__(cls, name, manifold)
|
||
|
|
||
|
obj.manifold.patches.append(obj) # deprecated
|
||
|
obj.coord_systems = _deprecated_list(
|
||
|
"""
|
||
|
Patch.coord_systms is deprecated. The Patch class is now
|
||
|
immutable. Instead use a separate list to keep track of coordinate
|
||
|
systems.
|
||
|
""", [])
|
||
|
return obj
|
||
|
|
||
|
@property
|
||
|
def name(self):
|
||
|
return self.args[0]
|
||
|
|
||
|
@property
|
||
|
def manifold(self):
|
||
|
return self.args[1]
|
||
|
|
||
|
@property
|
||
|
def dim(self):
|
||
|
return self.manifold.dim
|
||
|
|
||
|
|
||
|
class CoordSystem(Basic):
|
||
|
"""
|
||
|
A coordinate system defined on the patch.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Coordinate system is a system that uses one or more coordinates to uniquely
|
||
|
determine the position of the points or other geometric elements on a
|
||
|
manifold [1].
|
||
|
|
||
|
By passing ``Symbols`` to *symbols* parameter, user can define the name and
|
||
|
assumptions of coordinate symbols of the coordinate system. If not passed,
|
||
|
these symbols are generated automatically and are assumed to be real valued.
|
||
|
|
||
|
By passing *relations* parameter, user can define the transform relations of
|
||
|
coordinate systems. Inverse transformation and indirect transformation can
|
||
|
be found automatically. If this parameter is not passed, coordinate
|
||
|
transformation cannot be done.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
name : str
|
||
|
The name of the coordinate system.
|
||
|
|
||
|
patch : Patch
|
||
|
The patch where the coordinate system is defined.
|
||
|
|
||
|
symbols : list of Symbols, optional
|
||
|
Defines the names and assumptions of coordinate symbols.
|
||
|
|
||
|
relations : dict, optional
|
||
|
Key is a tuple of two strings, who are the names of the systems where
|
||
|
the coordinates transform from and transform to.
|
||
|
Value is a tuple of the symbols before transformation and a tuple of
|
||
|
the expressions after transformation.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
We define two-dimensional Cartesian coordinate system and polar coordinate
|
||
|
system.
|
||
|
|
||
|
>>> from sympy import symbols, pi, sqrt, atan2, cos, sin
|
||
|
>>> from sympy.diffgeom import Manifold, Patch, CoordSystem
|
||
|
>>> m = Manifold('M', 2)
|
||
|
>>> p = Patch('P', m)
|
||
|
>>> x, y = symbols('x y', real=True)
|
||
|
>>> r, theta = symbols('r theta', nonnegative=True)
|
||
|
>>> relation_dict = {
|
||
|
... ('Car2D', 'Pol'): [(x, y), (sqrt(x**2 + y**2), atan2(y, x))],
|
||
|
... ('Pol', 'Car2D'): [(r, theta), (r*cos(theta), r*sin(theta))]
|
||
|
... }
|
||
|
>>> Car2D = CoordSystem('Car2D', p, (x, y), relation_dict)
|
||
|
>>> Pol = CoordSystem('Pol', p, (r, theta), relation_dict)
|
||
|
|
||
|
``symbols`` property returns ``CoordinateSymbol`` instances. These symbols
|
||
|
are not same with the symbols used to construct the coordinate system.
|
||
|
|
||
|
>>> Car2D
|
||
|
Car2D
|
||
|
>>> Car2D.dim
|
||
|
2
|
||
|
>>> Car2D.symbols
|
||
|
(x, y)
|
||
|
>>> _[0].func
|
||
|
<class 'sympy.diffgeom.diffgeom.CoordinateSymbol'>
|
||
|
|
||
|
``transformation()`` method returns the transformation function from
|
||
|
one coordinate system to another. ``transform()`` method returns the
|
||
|
transformed coordinates.
|
||
|
|
||
|
>>> Car2D.transformation(Pol)
|
||
|
Lambda((x, y), Matrix([
|
||
|
[sqrt(x**2 + y**2)],
|
||
|
[ atan2(y, x)]]))
|
||
|
>>> Car2D.transform(Pol)
|
||
|
Matrix([
|
||
|
[sqrt(x**2 + y**2)],
|
||
|
[ atan2(y, x)]])
|
||
|
>>> Car2D.transform(Pol, [1, 2])
|
||
|
Matrix([
|
||
|
[sqrt(5)],
|
||
|
[atan(2)]])
|
||
|
|
||
|
``jacobian()`` method returns the Jacobian matrix of coordinate
|
||
|
transformation between two systems. ``jacobian_determinant()`` method
|
||
|
returns the Jacobian determinant of coordinate transformation between two
|
||
|
systems.
|
||
|
|
||
|
>>> Pol.jacobian(Car2D)
|
||
|
Matrix([
|
||
|
[cos(theta), -r*sin(theta)],
|
||
|
[sin(theta), r*cos(theta)]])
|
||
|
>>> Pol.jacobian(Car2D, [1, pi/2])
|
||
|
Matrix([
|
||
|
[0, -1],
|
||
|
[1, 0]])
|
||
|
>>> Car2D.jacobian_determinant(Pol)
|
||
|
1/sqrt(x**2 + y**2)
|
||
|
>>> Car2D.jacobian_determinant(Pol, [1,0])
|
||
|
1
|
||
|
|
||
|
References
|
||
|
==========
|
||
|
|
||
|
.. [1] https://en.wikipedia.org/wiki/Coordinate_system
|
||
|
|
||
|
"""
|
||
|
def __new__(cls, name, patch, symbols=None, relations={}, **kwargs):
|
||
|
if not isinstance(name, Str):
|
||
|
name = Str(name)
|
||
|
|
||
|
# canonicallize the symbols
|
||
|
if symbols is None:
|
||
|
names = kwargs.get('names', None)
|
||
|
if names is None:
|
||
|
symbols = Tuple(
|
||
|
*[Symbol('%s_%s' % (name.name, i), real=True)
|
||
|
for i in range(patch.dim)]
|
||
|
)
|
||
|
else:
|
||
|
sympy_deprecation_warning(
|
||
|
f"""
|
||
|
The 'names' argument to CoordSystem is deprecated. Use 'symbols' instead. That
|
||
|
is, replace
|
||
|
|
||
|
CoordSystem(..., names={names})
|
||
|
|
||
|
with
|
||
|
|
||
|
CoordSystem(..., symbols=[{', '.join(["Symbol(" + repr(n) + ", real=True)" for n in names])}])
|
||
|
""",
|
||
|
deprecated_since_version="1.7",
|
||
|
active_deprecations_target="deprecated-diffgeom-mutable",
|
||
|
)
|
||
|
symbols = Tuple(
|
||
|
*[Symbol(n, real=True) for n in names]
|
||
|
)
|
||
|
else:
|
||
|
syms = []
|
||
|
for s in symbols:
|
||
|
if isinstance(s, Symbol):
|
||
|
syms.append(Symbol(s.name, **s._assumptions.generator))
|
||
|
elif isinstance(s, str):
|
||
|
sympy_deprecation_warning(
|
||
|
f"""
|
||
|
|
||
|
Passing a string as the coordinate symbol name to CoordSystem is deprecated.
|
||
|
Pass a Symbol with the appropriate name and assumptions instead.
|
||
|
|
||
|
That is, replace {s} with Symbol({s!r}, real=True).
|
||
|
""",
|
||
|
|
||
|
deprecated_since_version="1.7",
|
||
|
active_deprecations_target="deprecated-diffgeom-mutable",
|
||
|
)
|
||
|
syms.append(Symbol(s, real=True))
|
||
|
symbols = Tuple(*syms)
|
||
|
|
||
|
# canonicallize the relations
|
||
|
rel_temp = {}
|
||
|
for k,v in relations.items():
|
||
|
s1, s2 = k
|
||
|
if not isinstance(s1, Str):
|
||
|
s1 = Str(s1)
|
||
|
if not isinstance(s2, Str):
|
||
|
s2 = Str(s2)
|
||
|
key = Tuple(s1, s2)
|
||
|
|
||
|
# Old version used Lambda as a value.
|
||
|
if isinstance(v, Lambda):
|
||
|
v = (tuple(v.signature), tuple(v.expr))
|
||
|
else:
|
||
|
v = (tuple(v[0]), tuple(v[1]))
|
||
|
rel_temp[key] = v
|
||
|
relations = Dict(rel_temp)
|
||
|
|
||
|
# construct the object
|
||
|
obj = super().__new__(cls, name, patch, symbols, relations)
|
||
|
|
||
|
# Add deprecated attributes
|
||
|
obj.transforms = _deprecated_dict(
|
||
|
"""
|
||
|
CoordSystem.transforms is deprecated. The CoordSystem class is now
|
||
|
immutable. Use the 'relations' keyword argument to the
|
||
|
CoordSystems() constructor to specify relations.
|
||
|
""", {})
|
||
|
obj._names = [str(n) for n in symbols]
|
||
|
obj.patch.coord_systems.append(obj) # deprecated
|
||
|
obj._dummies = [Dummy(str(n)) for n in symbols] # deprecated
|
||
|
obj._dummy = Dummy()
|
||
|
|
||
|
return obj
|
||
|
|
||
|
@property
|
||
|
def name(self):
|
||
|
return self.args[0]
|
||
|
|
||
|
@property
|
||
|
def patch(self):
|
||
|
return self.args[1]
|
||
|
|
||
|
@property
|
||
|
def manifold(self):
|
||
|
return self.patch.manifold
|
||
|
|
||
|
@property
|
||
|
def symbols(self):
|
||
|
return tuple(CoordinateSymbol(self, i, **s._assumptions.generator)
|
||
|
for i,s in enumerate(self.args[2]))
|
||
|
|
||
|
@property
|
||
|
def relations(self):
|
||
|
return self.args[3]
|
||
|
|
||
|
@property
|
||
|
def dim(self):
|
||
|
return self.patch.dim
|
||
|
|
||
|
##########################################################################
|
||
|
# Finding transformation relation
|
||
|
##########################################################################
|
||
|
|
||
|
def transformation(self, sys):
|
||
|
"""
|
||
|
Return coordinate transformation function from *self* to *sys*.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
sys : CoordSystem
|
||
|
|
||
|
Returns
|
||
|
=======
|
||
|
|
||
|
sympy.Lambda
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2_r, R2_p
|
||
|
>>> R2_r.transformation(R2_p)
|
||
|
Lambda((x, y), Matrix([
|
||
|
[sqrt(x**2 + y**2)],
|
||
|
[ atan2(y, x)]]))
|
||
|
|
||
|
"""
|
||
|
signature = self.args[2]
|
||
|
|
||
|
key = Tuple(self.name, sys.name)
|
||
|
if self == sys:
|
||
|
expr = Matrix(self.symbols)
|
||
|
elif key in self.relations:
|
||
|
expr = Matrix(self.relations[key][1])
|
||
|
elif key[::-1] in self.relations:
|
||
|
expr = Matrix(self._inverse_transformation(sys, self))
|
||
|
else:
|
||
|
expr = Matrix(self._indirect_transformation(self, sys))
|
||
|
return Lambda(signature, expr)
|
||
|
|
||
|
@staticmethod
|
||
|
def _solve_inverse(sym1, sym2, exprs, sys1_name, sys2_name):
|
||
|
ret = solve(
|
||
|
[t[0] - t[1] for t in zip(sym2, exprs)],
|
||
|
list(sym1), dict=True)
|
||
|
|
||
|
if len(ret) == 0:
|
||
|
temp = "Cannot solve inverse relation from {} to {}."
|
||
|
raise NotImplementedError(temp.format(sys1_name, sys2_name))
|
||
|
elif len(ret) > 1:
|
||
|
temp = "Obtained multiple inverse relation from {} to {}."
|
||
|
raise ValueError(temp.format(sys1_name, sys2_name))
|
||
|
|
||
|
return ret[0]
|
||
|
|
||
|
@classmethod
|
||
|
def _inverse_transformation(cls, sys1, sys2):
|
||
|
# Find the transformation relation from sys2 to sys1
|
||
|
forward = sys1.transform(sys2)
|
||
|
inv_results = cls._solve_inverse(sys1.symbols, sys2.symbols, forward,
|
||
|
sys1.name, sys2.name)
|
||
|
signature = tuple(sys1.symbols)
|
||
|
return [inv_results[s] for s in signature]
|
||
|
|
||
|
@classmethod
|
||
|
@cacheit
|
||
|
def _indirect_transformation(cls, sys1, sys2):
|
||
|
# Find the transformation relation between two indirectly connected
|
||
|
# coordinate systems
|
||
|
rel = sys1.relations
|
||
|
path = cls._dijkstra(sys1, sys2)
|
||
|
|
||
|
transforms = []
|
||
|
for s1, s2 in zip(path, path[1:]):
|
||
|
if (s1, s2) in rel:
|
||
|
transforms.append(rel[(s1, s2)])
|
||
|
else:
|
||
|
sym2, inv_exprs = rel[(s2, s1)]
|
||
|
sym1 = tuple(Dummy() for i in sym2)
|
||
|
ret = cls._solve_inverse(sym2, sym1, inv_exprs, s2, s1)
|
||
|
ret = tuple(ret[s] for s in sym2)
|
||
|
transforms.append((sym1, ret))
|
||
|
syms = sys1.args[2]
|
||
|
exprs = syms
|
||
|
for newsyms, newexprs in transforms:
|
||
|
exprs = tuple(e.subs(zip(newsyms, exprs)) for e in newexprs)
|
||
|
return exprs
|
||
|
|
||
|
@staticmethod
|
||
|
def _dijkstra(sys1, sys2):
|
||
|
# Use Dijkstra algorithm to find the shortest path between two indirectly-connected
|
||
|
# coordinate systems
|
||
|
# return value is the list of the names of the systems.
|
||
|
relations = sys1.relations
|
||
|
graph = {}
|
||
|
for s1, s2 in relations.keys():
|
||
|
if s1 not in graph:
|
||
|
graph[s1] = {s2}
|
||
|
else:
|
||
|
graph[s1].add(s2)
|
||
|
if s2 not in graph:
|
||
|
graph[s2] = {s1}
|
||
|
else:
|
||
|
graph[s2].add(s1)
|
||
|
|
||
|
path_dict = {sys:[0, [], 0] for sys in graph} # minimum distance, path, times of visited
|
||
|
|
||
|
def visit(sys):
|
||
|
path_dict[sys][2] = 1
|
||
|
for newsys in graph[sys]:
|
||
|
distance = path_dict[sys][0] + 1
|
||
|
if path_dict[newsys][0] >= distance or not path_dict[newsys][1]:
|
||
|
path_dict[newsys][0] = distance
|
||
|
path_dict[newsys][1] = list(path_dict[sys][1])
|
||
|
path_dict[newsys][1].append(sys)
|
||
|
|
||
|
visit(sys1.name)
|
||
|
|
||
|
while True:
|
||
|
min_distance = max(path_dict.values(), key=lambda x:x[0])[0]
|
||
|
newsys = None
|
||
|
for sys, lst in path_dict.items():
|
||
|
if 0 < lst[0] <= min_distance and not lst[2]:
|
||
|
min_distance = lst[0]
|
||
|
newsys = sys
|
||
|
if newsys is None:
|
||
|
break
|
||
|
visit(newsys)
|
||
|
|
||
|
result = path_dict[sys2.name][1]
|
||
|
result.append(sys2.name)
|
||
|
|
||
|
if result == [sys2.name]:
|
||
|
raise KeyError("Two coordinate systems are not connected.")
|
||
|
return result
|
||
|
|
||
|
def connect_to(self, to_sys, from_coords, to_exprs, inverse=True, fill_in_gaps=False):
|
||
|
sympy_deprecation_warning(
|
||
|
"""
|
||
|
The CoordSystem.connect_to() method is deprecated. Instead,
|
||
|
generate a new instance of CoordSystem with the 'relations'
|
||
|
keyword argument (CoordSystem classes are now immutable).
|
||
|
""",
|
||
|
deprecated_since_version="1.7",
|
||
|
active_deprecations_target="deprecated-diffgeom-mutable",
|
||
|
)
|
||
|
|
||
|
from_coords, to_exprs = dummyfy(from_coords, to_exprs)
|
||
|
self.transforms[to_sys] = Matrix(from_coords), Matrix(to_exprs)
|
||
|
|
||
|
if inverse:
|
||
|
to_sys.transforms[self] = self._inv_transf(from_coords, to_exprs)
|
||
|
|
||
|
if fill_in_gaps:
|
||
|
self._fill_gaps_in_transformations()
|
||
|
|
||
|
@staticmethod
|
||
|
def _inv_transf(from_coords, to_exprs):
|
||
|
# Will be removed when connect_to is removed
|
||
|
inv_from = [i.as_dummy() for i in from_coords]
|
||
|
inv_to = solve(
|
||
|
[t[0] - t[1] for t in zip(inv_from, to_exprs)],
|
||
|
list(from_coords), dict=True)[0]
|
||
|
inv_to = [inv_to[fc] for fc in from_coords]
|
||
|
return Matrix(inv_from), Matrix(inv_to)
|
||
|
|
||
|
@staticmethod
|
||
|
def _fill_gaps_in_transformations():
|
||
|
# Will be removed when connect_to is removed
|
||
|
raise NotImplementedError
|
||
|
|
||
|
##########################################################################
|
||
|
# Coordinate transformations
|
||
|
##########################################################################
|
||
|
|
||
|
def transform(self, sys, coordinates=None):
|
||
|
"""
|
||
|
Return the result of coordinate transformation from *self* to *sys*.
|
||
|
If coordinates are not given, coordinate symbols of *self* are used.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
sys : CoordSystem
|
||
|
|
||
|
coordinates : Any iterable, optional.
|
||
|
|
||
|
Returns
|
||
|
=======
|
||
|
|
||
|
sympy.ImmutableDenseMatrix containing CoordinateSymbol
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2_r, R2_p
|
||
|
>>> R2_r.transform(R2_p)
|
||
|
Matrix([
|
||
|
[sqrt(x**2 + y**2)],
|
||
|
[ atan2(y, x)]])
|
||
|
>>> R2_r.transform(R2_p, [0, 1])
|
||
|
Matrix([
|
||
|
[ 1],
|
||
|
[pi/2]])
|
||
|
|
||
|
"""
|
||
|
if coordinates is None:
|
||
|
coordinates = self.symbols
|
||
|
if self != sys:
|
||
|
transf = self.transformation(sys)
|
||
|
coordinates = transf(*coordinates)
|
||
|
else:
|
||
|
coordinates = Matrix(coordinates)
|
||
|
return coordinates
|
||
|
|
||
|
def coord_tuple_transform_to(self, to_sys, coords):
|
||
|
"""Transform ``coords`` to coord system ``to_sys``."""
|
||
|
sympy_deprecation_warning(
|
||
|
"""
|
||
|
The CoordSystem.coord_tuple_transform_to() method is deprecated.
|
||
|
Use the CoordSystem.transform() method instead.
|
||
|
""",
|
||
|
deprecated_since_version="1.7",
|
||
|
active_deprecations_target="deprecated-diffgeom-mutable",
|
||
|
)
|
||
|
|
||
|
coords = Matrix(coords)
|
||
|
if self != to_sys:
|
||
|
with ignore_warnings(SymPyDeprecationWarning):
|
||
|
transf = self.transforms[to_sys]
|
||
|
coords = transf[1].subs(list(zip(transf[0], coords)))
|
||
|
return coords
|
||
|
|
||
|
def jacobian(self, sys, coordinates=None):
|
||
|
"""
|
||
|
Return the jacobian matrix of a transformation on given coordinates.
|
||
|
If coordinates are not given, coordinate symbols of *self* are used.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
sys : CoordSystem
|
||
|
|
||
|
coordinates : Any iterable, optional.
|
||
|
|
||
|
Returns
|
||
|
=======
|
||
|
|
||
|
sympy.ImmutableDenseMatrix
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2_r, R2_p
|
||
|
>>> R2_p.jacobian(R2_r)
|
||
|
Matrix([
|
||
|
[cos(theta), -rho*sin(theta)],
|
||
|
[sin(theta), rho*cos(theta)]])
|
||
|
>>> R2_p.jacobian(R2_r, [1, 0])
|
||
|
Matrix([
|
||
|
[1, 0],
|
||
|
[0, 1]])
|
||
|
|
||
|
"""
|
||
|
result = self.transform(sys).jacobian(self.symbols)
|
||
|
if coordinates is not None:
|
||
|
result = result.subs(list(zip(self.symbols, coordinates)))
|
||
|
return result
|
||
|
jacobian_matrix = jacobian
|
||
|
|
||
|
def jacobian_determinant(self, sys, coordinates=None):
|
||
|
"""
|
||
|
Return the jacobian determinant of a transformation on given
|
||
|
coordinates. If coordinates are not given, coordinate symbols of *self*
|
||
|
are used.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
sys : CoordSystem
|
||
|
|
||
|
coordinates : Any iterable, optional.
|
||
|
|
||
|
Returns
|
||
|
=======
|
||
|
|
||
|
sympy.Expr
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2_r, R2_p
|
||
|
>>> R2_r.jacobian_determinant(R2_p)
|
||
|
1/sqrt(x**2 + y**2)
|
||
|
>>> R2_r.jacobian_determinant(R2_p, [1, 0])
|
||
|
1
|
||
|
|
||
|
"""
|
||
|
return self.jacobian(sys, coordinates).det()
|
||
|
|
||
|
|
||
|
##########################################################################
|
||
|
# Points
|
||
|
##########################################################################
|
||
|
|
||
|
def point(self, coords):
|
||
|
"""Create a ``Point`` with coordinates given in this coord system."""
|
||
|
return Point(self, coords)
|
||
|
|
||
|
def point_to_coords(self, point):
|
||
|
"""Calculate the coordinates of a point in this coord system."""
|
||
|
return point.coords(self)
|
||
|
|
||
|
##########################################################################
|
||
|
# Base fields.
|
||
|
##########################################################################
|
||
|
|
||
|
def base_scalar(self, coord_index):
|
||
|
"""Return ``BaseScalarField`` that takes a point and returns one of the coordinates."""
|
||
|
return BaseScalarField(self, coord_index)
|
||
|
coord_function = base_scalar
|
||
|
|
||
|
def base_scalars(self):
|
||
|
"""Returns a list of all coordinate functions.
|
||
|
For more details see the ``base_scalar`` method of this class."""
|
||
|
return [self.base_scalar(i) for i in range(self.dim)]
|
||
|
coord_functions = base_scalars
|
||
|
|
||
|
def base_vector(self, coord_index):
|
||
|
"""Return a basis vector field.
|
||
|
The basis vector field for this coordinate system. It is also an
|
||
|
operator on scalar fields."""
|
||
|
return BaseVectorField(self, coord_index)
|
||
|
|
||
|
def base_vectors(self):
|
||
|
"""Returns a list of all base vectors.
|
||
|
For more details see the ``base_vector`` method of this class."""
|
||
|
return [self.base_vector(i) for i in range(self.dim)]
|
||
|
|
||
|
def base_oneform(self, coord_index):
|
||
|
"""Return a basis 1-form field.
|
||
|
The basis one-form field for this coordinate system. It is also an
|
||
|
operator on vector fields."""
|
||
|
return Differential(self.coord_function(coord_index))
|
||
|
|
||
|
def base_oneforms(self):
|
||
|
"""Returns a list of all base oneforms.
|
||
|
For more details see the ``base_oneform`` method of this class."""
|
||
|
return [self.base_oneform(i) for i in range(self.dim)]
|
||
|
|
||
|
|
||
|
class CoordinateSymbol(Symbol):
|
||
|
"""A symbol which denotes an abstract value of i-th coordinate of
|
||
|
the coordinate system with given context.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Each coordinates in coordinate system are represented by unique symbol,
|
||
|
such as x, y, z in Cartesian coordinate system.
|
||
|
|
||
|
You may not construct this class directly. Instead, use `symbols` method
|
||
|
of CoordSystem.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
coord_sys : CoordSystem
|
||
|
|
||
|
index : integer
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import symbols, Lambda, Matrix, sqrt, atan2, cos, sin
|
||
|
>>> from sympy.diffgeom import Manifold, Patch, CoordSystem
|
||
|
>>> m = Manifold('M', 2)
|
||
|
>>> p = Patch('P', m)
|
||
|
>>> x, y = symbols('x y', real=True)
|
||
|
>>> r, theta = symbols('r theta', nonnegative=True)
|
||
|
>>> relation_dict = {
|
||
|
... ('Car2D', 'Pol'): Lambda((x, y), Matrix([sqrt(x**2 + y**2), atan2(y, x)])),
|
||
|
... ('Pol', 'Car2D'): Lambda((r, theta), Matrix([r*cos(theta), r*sin(theta)]))
|
||
|
... }
|
||
|
>>> Car2D = CoordSystem('Car2D', p, [x, y], relation_dict)
|
||
|
>>> Pol = CoordSystem('Pol', p, [r, theta], relation_dict)
|
||
|
>>> x, y = Car2D.symbols
|
||
|
|
||
|
``CoordinateSymbol`` contains its coordinate symbol and index.
|
||
|
|
||
|
>>> x.name
|
||
|
'x'
|
||
|
>>> x.coord_sys == Car2D
|
||
|
True
|
||
|
>>> x.index
|
||
|
0
|
||
|
>>> x.is_real
|
||
|
True
|
||
|
|
||
|
You can transform ``CoordinateSymbol`` into other coordinate system using
|
||
|
``rewrite()`` method.
|
||
|
|
||
|
>>> x.rewrite(Pol)
|
||
|
r*cos(theta)
|
||
|
>>> sqrt(x**2 + y**2).rewrite(Pol).simplify()
|
||
|
r
|
||
|
|
||
|
"""
|
||
|
def __new__(cls, coord_sys, index, **assumptions):
|
||
|
name = coord_sys.args[2][index].name
|
||
|
obj = super().__new__(cls, name, **assumptions)
|
||
|
obj.coord_sys = coord_sys
|
||
|
obj.index = index
|
||
|
return obj
|
||
|
|
||
|
def __getnewargs__(self):
|
||
|
return (self.coord_sys, self.index)
|
||
|
|
||
|
def _hashable_content(self):
|
||
|
return (
|
||
|
self.coord_sys, self.index
|
||
|
) + tuple(sorted(self.assumptions0.items()))
|
||
|
|
||
|
def _eval_rewrite(self, rule, args, **hints):
|
||
|
if isinstance(rule, CoordSystem):
|
||
|
return rule.transform(self.coord_sys)[self.index]
|
||
|
return super()._eval_rewrite(rule, args, **hints)
|
||
|
|
||
|
|
||
|
class Point(Basic):
|
||
|
"""Point defined in a coordinate system.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Mathematically, point is defined in the manifold and does not have any coordinates
|
||
|
by itself. Coordinate system is what imbues the coordinates to the point by coordinate
|
||
|
chart. However, due to the difficulty of realizing such logic, you must supply
|
||
|
a coordinate system and coordinates to define a Point here.
|
||
|
|
||
|
The usage of this object after its definition is independent of the
|
||
|
coordinate system that was used in order to define it, however due to
|
||
|
limitations in the simplification routines you can arrive at complicated
|
||
|
expressions if you use inappropriate coordinate systems.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
coord_sys : CoordSystem
|
||
|
|
||
|
coords : list
|
||
|
The coordinates of the point.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import pi
|
||
|
>>> from sympy.diffgeom import Point
|
||
|
>>> from sympy.diffgeom.rn import R2, R2_r, R2_p
|
||
|
>>> rho, theta = R2_p.symbols
|
||
|
|
||
|
>>> p = Point(R2_p, [rho, 3*pi/4])
|
||
|
|
||
|
>>> p.manifold == R2
|
||
|
True
|
||
|
|
||
|
>>> p.coords()
|
||
|
Matrix([
|
||
|
[ rho],
|
||
|
[3*pi/4]])
|
||
|
>>> p.coords(R2_r)
|
||
|
Matrix([
|
||
|
[-sqrt(2)*rho/2],
|
||
|
[ sqrt(2)*rho/2]])
|
||
|
|
||
|
"""
|
||
|
|
||
|
def __new__(cls, coord_sys, coords, **kwargs):
|
||
|
coords = Matrix(coords)
|
||
|
obj = super().__new__(cls, coord_sys, coords)
|
||
|
obj._coord_sys = coord_sys
|
||
|
obj._coords = coords
|
||
|
return obj
|
||
|
|
||
|
@property
|
||
|
def patch(self):
|
||
|
return self._coord_sys.patch
|
||
|
|
||
|
@property
|
||
|
def manifold(self):
|
||
|
return self._coord_sys.manifold
|
||
|
|
||
|
@property
|
||
|
def dim(self):
|
||
|
return self.manifold.dim
|
||
|
|
||
|
def coords(self, sys=None):
|
||
|
"""
|
||
|
Coordinates of the point in given coordinate system. If coordinate system
|
||
|
is not passed, it returns the coordinates in the coordinate system in which
|
||
|
the poin was defined.
|
||
|
"""
|
||
|
if sys is None:
|
||
|
return self._coords
|
||
|
else:
|
||
|
return self._coord_sys.transform(sys, self._coords)
|
||
|
|
||
|
@property
|
||
|
def free_symbols(self):
|
||
|
return self._coords.free_symbols
|
||
|
|
||
|
|
||
|
class BaseScalarField(Expr):
|
||
|
"""Base scalar field over a manifold for a given coordinate system.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
A scalar field takes a point as an argument and returns a scalar.
|
||
|
A base scalar field of a coordinate system takes a point and returns one of
|
||
|
the coordinates of that point in the coordinate system in question.
|
||
|
|
||
|
To define a scalar field you need to choose the coordinate system and the
|
||
|
index of the coordinate.
|
||
|
|
||
|
The use of the scalar field after its definition is independent of the
|
||
|
coordinate system in which it was defined, however due to limitations in
|
||
|
the simplification routines you may arrive at more complicated
|
||
|
expression if you use unappropriate coordinate systems.
|
||
|
You can build complicated scalar fields by just building up SymPy
|
||
|
expressions containing ``BaseScalarField`` instances.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
coord_sys : CoordSystem
|
||
|
|
||
|
index : integer
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import Function, pi
|
||
|
>>> from sympy.diffgeom import BaseScalarField
|
||
|
>>> from sympy.diffgeom.rn import R2_r, R2_p
|
||
|
>>> rho, _ = R2_p.symbols
|
||
|
>>> point = R2_p.point([rho, 0])
|
||
|
>>> fx, fy = R2_r.base_scalars()
|
||
|
>>> ftheta = BaseScalarField(R2_r, 1)
|
||
|
|
||
|
>>> fx(point)
|
||
|
rho
|
||
|
>>> fy(point)
|
||
|
0
|
||
|
|
||
|
>>> (fx**2+fy**2).rcall(point)
|
||
|
rho**2
|
||
|
|
||
|
>>> g = Function('g')
|
||
|
>>> fg = g(ftheta-pi)
|
||
|
>>> fg.rcall(point)
|
||
|
g(-pi)
|
||
|
|
||
|
"""
|
||
|
|
||
|
is_commutative = True
|
||
|
|
||
|
def __new__(cls, coord_sys, index, **kwargs):
|
||
|
index = _sympify(index)
|
||
|
obj = super().__new__(cls, coord_sys, index)
|
||
|
obj._coord_sys = coord_sys
|
||
|
obj._index = index
|
||
|
return obj
|
||
|
|
||
|
@property
|
||
|
def coord_sys(self):
|
||
|
return self.args[0]
|
||
|
|
||
|
@property
|
||
|
def index(self):
|
||
|
return self.args[1]
|
||
|
|
||
|
@property
|
||
|
def patch(self):
|
||
|
return self.coord_sys.patch
|
||
|
|
||
|
@property
|
||
|
def manifold(self):
|
||
|
return self.coord_sys.manifold
|
||
|
|
||
|
@property
|
||
|
def dim(self):
|
||
|
return self.manifold.dim
|
||
|
|
||
|
def __call__(self, *args):
|
||
|
"""Evaluating the field at a point or doing nothing.
|
||
|
If the argument is a ``Point`` instance, the field is evaluated at that
|
||
|
point. The field is returned itself if the argument is any other
|
||
|
object. It is so in order to have working recursive calling mechanics
|
||
|
for all fields (check the ``__call__`` method of ``Expr``).
|
||
|
"""
|
||
|
point = args[0]
|
||
|
if len(args) != 1 or not isinstance(point, Point):
|
||
|
return self
|
||
|
coords = point.coords(self._coord_sys)
|
||
|
# XXX Calling doit is necessary with all the Subs expressions
|
||
|
# XXX Calling simplify is necessary with all the trig expressions
|
||
|
return simplify(coords[self._index]).doit()
|
||
|
|
||
|
# XXX Workaround for limitations on the content of args
|
||
|
free_symbols: set[Any] = set()
|
||
|
|
||
|
|
||
|
class BaseVectorField(Expr):
|
||
|
r"""Base vector field over a manifold for a given coordinate system.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
A vector field is an operator taking a scalar field and returning a
|
||
|
directional derivative (which is also a scalar field).
|
||
|
A base vector field is the same type of operator, however the derivation is
|
||
|
specifically done with respect to a chosen coordinate.
|
||
|
|
||
|
To define a base vector field you need to choose the coordinate system and
|
||
|
the index of the coordinate.
|
||
|
|
||
|
The use of the vector field after its definition is independent of the
|
||
|
coordinate system in which it was defined, however due to limitations in the
|
||
|
simplification routines you may arrive at more complicated expression if you
|
||
|
use unappropriate coordinate systems.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
coord_sys : CoordSystem
|
||
|
|
||
|
index : integer
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import Function
|
||
|
>>> from sympy.diffgeom.rn import R2_p, R2_r
|
||
|
>>> from sympy.diffgeom import BaseVectorField
|
||
|
>>> from sympy import pprint
|
||
|
|
||
|
>>> x, y = R2_r.symbols
|
||
|
>>> rho, theta = R2_p.symbols
|
||
|
>>> fx, fy = R2_r.base_scalars()
|
||
|
>>> point_p = R2_p.point([rho, theta])
|
||
|
>>> point_r = R2_r.point([x, y])
|
||
|
|
||
|
>>> g = Function('g')
|
||
|
>>> s_field = g(fx, fy)
|
||
|
|
||
|
>>> v = BaseVectorField(R2_r, 1)
|
||
|
>>> pprint(v(s_field))
|
||
|
/ d \|
|
||
|
|---(g(x, xi))||
|
||
|
\dxi /|xi=y
|
||
|
>>> pprint(v(s_field).rcall(point_r).doit())
|
||
|
d
|
||
|
--(g(x, y))
|
||
|
dy
|
||
|
>>> pprint(v(s_field).rcall(point_p))
|
||
|
/ d \|
|
||
|
|---(g(rho*cos(theta), xi))||
|
||
|
\dxi /|xi=rho*sin(theta)
|
||
|
|
||
|
"""
|
||
|
|
||
|
is_commutative = False
|
||
|
|
||
|
def __new__(cls, coord_sys, index, **kwargs):
|
||
|
index = _sympify(index)
|
||
|
obj = super().__new__(cls, coord_sys, index)
|
||
|
obj._coord_sys = coord_sys
|
||
|
obj._index = index
|
||
|
return obj
|
||
|
|
||
|
@property
|
||
|
def coord_sys(self):
|
||
|
return self.args[0]
|
||
|
|
||
|
@property
|
||
|
def index(self):
|
||
|
return self.args[1]
|
||
|
|
||
|
@property
|
||
|
def patch(self):
|
||
|
return self.coord_sys.patch
|
||
|
|
||
|
@property
|
||
|
def manifold(self):
|
||
|
return self.coord_sys.manifold
|
||
|
|
||
|
@property
|
||
|
def dim(self):
|
||
|
return self.manifold.dim
|
||
|
|
||
|
def __call__(self, scalar_field):
|
||
|
"""Apply on a scalar field.
|
||
|
The action of a vector field on a scalar field is a directional
|
||
|
differentiation.
|
||
|
If the argument is not a scalar field an error is raised.
|
||
|
"""
|
||
|
if covariant_order(scalar_field) or contravariant_order(scalar_field):
|
||
|
raise ValueError('Only scalar fields can be supplied as arguments to vector fields.')
|
||
|
|
||
|
if scalar_field is None:
|
||
|
return self
|
||
|
|
||
|
base_scalars = list(scalar_field.atoms(BaseScalarField))
|
||
|
|
||
|
# First step: e_x(x+r**2) -> e_x(x) + 2*r*e_x(r)
|
||
|
d_var = self._coord_sys._dummy
|
||
|
# TODO: you need a real dummy function for the next line
|
||
|
d_funcs = [Function('_#_%s' % i)(d_var) for i,
|
||
|
b in enumerate(base_scalars)]
|
||
|
d_result = scalar_field.subs(list(zip(base_scalars, d_funcs)))
|
||
|
d_result = d_result.diff(d_var)
|
||
|
|
||
|
# Second step: e_x(x) -> 1 and e_x(r) -> cos(atan2(x, y))
|
||
|
coords = self._coord_sys.symbols
|
||
|
d_funcs_deriv = [f.diff(d_var) for f in d_funcs]
|
||
|
d_funcs_deriv_sub = []
|
||
|
for b in base_scalars:
|
||
|
jac = self._coord_sys.jacobian(b._coord_sys, coords)
|
||
|
d_funcs_deriv_sub.append(jac[b._index, self._index])
|
||
|
d_result = d_result.subs(list(zip(d_funcs_deriv, d_funcs_deriv_sub)))
|
||
|
|
||
|
# Remove the dummies
|
||
|
result = d_result.subs(list(zip(d_funcs, base_scalars)))
|
||
|
result = result.subs(list(zip(coords, self._coord_sys.coord_functions())))
|
||
|
return result.doit()
|
||
|
|
||
|
|
||
|
def _find_coords(expr):
|
||
|
# Finds CoordinateSystems existing in expr
|
||
|
fields = expr.atoms(BaseScalarField, BaseVectorField)
|
||
|
result = set()
|
||
|
for f in fields:
|
||
|
result.add(f._coord_sys)
|
||
|
return result
|
||
|
|
||
|
|
||
|
class Commutator(Expr):
|
||
|
r"""Commutator of two vector fields.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
The commutator of two vector fields `v_1` and `v_2` is defined as the
|
||
|
vector field `[v_1, v_2]` that evaluated on each scalar field `f` is equal
|
||
|
to `v_1(v_2(f)) - v_2(v_1(f))`.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2_p, R2_r
|
||
|
>>> from sympy.diffgeom import Commutator
|
||
|
>>> from sympy import simplify
|
||
|
|
||
|
>>> fx, fy = R2_r.base_scalars()
|
||
|
>>> e_x, e_y = R2_r.base_vectors()
|
||
|
>>> e_r = R2_p.base_vector(0)
|
||
|
|
||
|
>>> c_xy = Commutator(e_x, e_y)
|
||
|
>>> c_xr = Commutator(e_x, e_r)
|
||
|
>>> c_xy
|
||
|
0
|
||
|
|
||
|
Unfortunately, the current code is not able to compute everything:
|
||
|
|
||
|
>>> c_xr
|
||
|
Commutator(e_x, e_rho)
|
||
|
>>> simplify(c_xr(fy**2))
|
||
|
-2*cos(theta)*y**2/(x**2 + y**2)
|
||
|
|
||
|
"""
|
||
|
def __new__(cls, v1, v2):
|
||
|
if (covariant_order(v1) or contravariant_order(v1) != 1
|
||
|
or covariant_order(v2) or contravariant_order(v2) != 1):
|
||
|
raise ValueError(
|
||
|
'Only commutators of vector fields are supported.')
|
||
|
if v1 == v2:
|
||
|
return S.Zero
|
||
|
coord_sys = set().union(*[_find_coords(v) for v in (v1, v2)])
|
||
|
if len(coord_sys) == 1:
|
||
|
# Only one coordinate systems is used, hence it is easy enough to
|
||
|
# actually evaluate the commutator.
|
||
|
if all(isinstance(v, BaseVectorField) for v in (v1, v2)):
|
||
|
return S.Zero
|
||
|
bases_1, bases_2 = [list(v.atoms(BaseVectorField))
|
||
|
for v in (v1, v2)]
|
||
|
coeffs_1 = [v1.expand().coeff(b) for b in bases_1]
|
||
|
coeffs_2 = [v2.expand().coeff(b) for b in bases_2]
|
||
|
res = 0
|
||
|
for c1, b1 in zip(coeffs_1, bases_1):
|
||
|
for c2, b2 in zip(coeffs_2, bases_2):
|
||
|
res += c1*b1(c2)*b2 - c2*b2(c1)*b1
|
||
|
return res
|
||
|
else:
|
||
|
obj = super().__new__(cls, v1, v2)
|
||
|
obj._v1 = v1 # deprecated assignment
|
||
|
obj._v2 = v2 # deprecated assignment
|
||
|
return obj
|
||
|
|
||
|
@property
|
||
|
def v1(self):
|
||
|
return self.args[0]
|
||
|
|
||
|
@property
|
||
|
def v2(self):
|
||
|
return self.args[1]
|
||
|
|
||
|
def __call__(self, scalar_field):
|
||
|
"""Apply on a scalar field.
|
||
|
If the argument is not a scalar field an error is raised.
|
||
|
"""
|
||
|
return self.v1(self.v2(scalar_field)) - self.v2(self.v1(scalar_field))
|
||
|
|
||
|
|
||
|
class Differential(Expr):
|
||
|
r"""Return the differential (exterior derivative) of a form field.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
The differential of a form (i.e. the exterior derivative) has a complicated
|
||
|
definition in the general case.
|
||
|
The differential `df` of the 0-form `f` is defined for any vector field `v`
|
||
|
as `df(v) = v(f)`.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import Function
|
||
|
>>> from sympy.diffgeom.rn import R2_r
|
||
|
>>> from sympy.diffgeom import Differential
|
||
|
>>> from sympy import pprint
|
||
|
|
||
|
>>> fx, fy = R2_r.base_scalars()
|
||
|
>>> e_x, e_y = R2_r.base_vectors()
|
||
|
>>> g = Function('g')
|
||
|
>>> s_field = g(fx, fy)
|
||
|
>>> dg = Differential(s_field)
|
||
|
|
||
|
>>> dg
|
||
|
d(g(x, y))
|
||
|
>>> pprint(dg(e_x))
|
||
|
/ d \|
|
||
|
|---(g(xi, y))||
|
||
|
\dxi /|xi=x
|
||
|
>>> pprint(dg(e_y))
|
||
|
/ d \|
|
||
|
|---(g(x, xi))||
|
||
|
\dxi /|xi=y
|
||
|
|
||
|
Applying the exterior derivative operator twice always results in:
|
||
|
|
||
|
>>> Differential(dg)
|
||
|
0
|
||
|
"""
|
||
|
|
||
|
is_commutative = False
|
||
|
|
||
|
def __new__(cls, form_field):
|
||
|
if contravariant_order(form_field):
|
||
|
raise ValueError(
|
||
|
'A vector field was supplied as an argument to Differential.')
|
||
|
if isinstance(form_field, Differential):
|
||
|
return S.Zero
|
||
|
else:
|
||
|
obj = super().__new__(cls, form_field)
|
||
|
obj._form_field = form_field # deprecated assignment
|
||
|
return obj
|
||
|
|
||
|
@property
|
||
|
def form_field(self):
|
||
|
return self.args[0]
|
||
|
|
||
|
def __call__(self, *vector_fields):
|
||
|
"""Apply on a list of vector_fields.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
If the number of vector fields supplied is not equal to 1 + the order of
|
||
|
the form field inside the differential the result is undefined.
|
||
|
|
||
|
For 1-forms (i.e. differentials of scalar fields) the evaluation is
|
||
|
done as `df(v)=v(f)`. However if `v` is ``None`` instead of a vector
|
||
|
field, the differential is returned unchanged. This is done in order to
|
||
|
permit partial contractions for higher forms.
|
||
|
|
||
|
In the general case the evaluation is done by applying the form field
|
||
|
inside the differential on a list with one less elements than the number
|
||
|
of elements in the original list. Lowering the number of vector fields
|
||
|
is achieved through replacing each pair of fields by their
|
||
|
commutator.
|
||
|
|
||
|
If the arguments are not vectors or ``None``s an error is raised.
|
||
|
"""
|
||
|
if any((contravariant_order(a) != 1 or covariant_order(a)) and a is not None
|
||
|
for a in vector_fields):
|
||
|
raise ValueError('The arguments supplied to Differential should be vector fields or Nones.')
|
||
|
k = len(vector_fields)
|
||
|
if k == 1:
|
||
|
if vector_fields[0]:
|
||
|
return vector_fields[0].rcall(self._form_field)
|
||
|
return self
|
||
|
else:
|
||
|
# For higher form it is more complicated:
|
||
|
# Invariant formula:
|
||
|
# https://en.wikipedia.org/wiki/Exterior_derivative#Invariant_formula
|
||
|
# df(v1, ... vn) = +/- vi(f(v1..no i..vn))
|
||
|
# +/- f([vi,vj],v1..no i, no j..vn)
|
||
|
f = self._form_field
|
||
|
v = vector_fields
|
||
|
ret = 0
|
||
|
for i in range(k):
|
||
|
t = v[i].rcall(f.rcall(*v[:i] + v[i + 1:]))
|
||
|
ret += (-1)**i*t
|
||
|
for j in range(i + 1, k):
|
||
|
c = Commutator(v[i], v[j])
|
||
|
if c: # TODO this is ugly - the Commutator can be Zero and
|
||
|
# this causes the next line to fail
|
||
|
t = f.rcall(*(c,) + v[:i] + v[i + 1:j] + v[j + 1:])
|
||
|
ret += (-1)**(i + j)*t
|
||
|
return ret
|
||
|
|
||
|
|
||
|
class TensorProduct(Expr):
|
||
|
"""Tensor product of forms.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
The tensor product permits the creation of multilinear functionals (i.e.
|
||
|
higher order tensors) out of lower order fields (e.g. 1-forms and vector
|
||
|
fields). However, the higher tensors thus created lack the interesting
|
||
|
features provided by the other type of product, the wedge product, namely
|
||
|
they are not antisymmetric and hence are not form fields.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2_r
|
||
|
>>> from sympy.diffgeom import TensorProduct
|
||
|
|
||
|
>>> fx, fy = R2_r.base_scalars()
|
||
|
>>> e_x, e_y = R2_r.base_vectors()
|
||
|
>>> dx, dy = R2_r.base_oneforms()
|
||
|
|
||
|
>>> TensorProduct(dx, dy)(e_x, e_y)
|
||
|
1
|
||
|
>>> TensorProduct(dx, dy)(e_y, e_x)
|
||
|
0
|
||
|
>>> TensorProduct(dx, fx*dy)(fx*e_x, e_y)
|
||
|
x**2
|
||
|
>>> TensorProduct(e_x, e_y)(fx**2, fy**2)
|
||
|
4*x*y
|
||
|
>>> TensorProduct(e_y, dx)(fy)
|
||
|
dx
|
||
|
|
||
|
You can nest tensor products.
|
||
|
|
||
|
>>> tp1 = TensorProduct(dx, dy)
|
||
|
>>> TensorProduct(tp1, dx)(e_x, e_y, e_x)
|
||
|
1
|
||
|
|
||
|
You can make partial contraction for instance when 'raising an index'.
|
||
|
Putting ``None`` in the second argument of ``rcall`` means that the
|
||
|
respective position in the tensor product is left as it is.
|
||
|
|
||
|
>>> TP = TensorProduct
|
||
|
>>> metric = TP(dx, dx) + 3*TP(dy, dy)
|
||
|
>>> metric.rcall(e_y, None)
|
||
|
3*dy
|
||
|
|
||
|
Or automatically pad the args with ``None`` without specifying them.
|
||
|
|
||
|
>>> metric.rcall(e_y)
|
||
|
3*dy
|
||
|
|
||
|
"""
|
||
|
def __new__(cls, *args):
|
||
|
scalar = Mul(*[m for m in args if covariant_order(m) + contravariant_order(m) == 0])
|
||
|
multifields = [m for m in args if covariant_order(m) + contravariant_order(m)]
|
||
|
if multifields:
|
||
|
if len(multifields) == 1:
|
||
|
return scalar*multifields[0]
|
||
|
return scalar*super().__new__(cls, *multifields)
|
||
|
else:
|
||
|
return scalar
|
||
|
|
||
|
def __call__(self, *fields):
|
||
|
"""Apply on a list of fields.
|
||
|
|
||
|
If the number of input fields supplied is not equal to the order of
|
||
|
the tensor product field, the list of arguments is padded with ``None``'s.
|
||
|
|
||
|
The list of arguments is divided in sublists depending on the order of
|
||
|
the forms inside the tensor product. The sublists are provided as
|
||
|
arguments to these forms and the resulting expressions are given to the
|
||
|
constructor of ``TensorProduct``.
|
||
|
|
||
|
"""
|
||
|
tot_order = covariant_order(self) + contravariant_order(self)
|
||
|
tot_args = len(fields)
|
||
|
if tot_args != tot_order:
|
||
|
fields = list(fields) + [None]*(tot_order - tot_args)
|
||
|
orders = [covariant_order(f) + contravariant_order(f) for f in self._args]
|
||
|
indices = [sum(orders[:i + 1]) for i in range(len(orders) - 1)]
|
||
|
fields = [fields[i:j] for i, j in zip([0] + indices, indices + [None])]
|
||
|
multipliers = [t[0].rcall(*t[1]) for t in zip(self._args, fields)]
|
||
|
return TensorProduct(*multipliers)
|
||
|
|
||
|
|
||
|
class WedgeProduct(TensorProduct):
|
||
|
"""Wedge product of forms.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
In the context of integration only completely antisymmetric forms make
|
||
|
sense. The wedge product permits the creation of such forms.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2_r
|
||
|
>>> from sympy.diffgeom import WedgeProduct
|
||
|
|
||
|
>>> fx, fy = R2_r.base_scalars()
|
||
|
>>> e_x, e_y = R2_r.base_vectors()
|
||
|
>>> dx, dy = R2_r.base_oneforms()
|
||
|
|
||
|
>>> WedgeProduct(dx, dy)(e_x, e_y)
|
||
|
1
|
||
|
>>> WedgeProduct(dx, dy)(e_y, e_x)
|
||
|
-1
|
||
|
>>> WedgeProduct(dx, fx*dy)(fx*e_x, e_y)
|
||
|
x**2
|
||
|
>>> WedgeProduct(e_x, e_y)(fy, None)
|
||
|
-e_x
|
||
|
|
||
|
You can nest wedge products.
|
||
|
|
||
|
>>> wp1 = WedgeProduct(dx, dy)
|
||
|
>>> WedgeProduct(wp1, dx)(e_x, e_y, e_x)
|
||
|
0
|
||
|
|
||
|
"""
|
||
|
# TODO the calculation of signatures is slow
|
||
|
# TODO you do not need all these permutations (neither the prefactor)
|
||
|
def __call__(self, *fields):
|
||
|
"""Apply on a list of vector_fields.
|
||
|
The expression is rewritten internally in terms of tensor products and evaluated."""
|
||
|
orders = (covariant_order(e) + contravariant_order(e) for e in self.args)
|
||
|
mul = 1/Mul(*(factorial(o) for o in orders))
|
||
|
perms = permutations(fields)
|
||
|
perms_par = (Permutation(
|
||
|
p).signature() for p in permutations(range(len(fields))))
|
||
|
tensor_prod = TensorProduct(*self.args)
|
||
|
return mul*Add(*[tensor_prod(*p[0])*p[1] for p in zip(perms, perms_par)])
|
||
|
|
||
|
|
||
|
class LieDerivative(Expr):
|
||
|
"""Lie derivative with respect to a vector field.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
The transport operator that defines the Lie derivative is the pushforward of
|
||
|
the field to be derived along the integral curve of the field with respect
|
||
|
to which one derives.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2_r, R2_p
|
||
|
>>> from sympy.diffgeom import (LieDerivative, TensorProduct)
|
||
|
|
||
|
>>> fx, fy = R2_r.base_scalars()
|
||
|
>>> e_x, e_y = R2_r.base_vectors()
|
||
|
>>> e_rho, e_theta = R2_p.base_vectors()
|
||
|
>>> dx, dy = R2_r.base_oneforms()
|
||
|
|
||
|
>>> LieDerivative(e_x, fy)
|
||
|
0
|
||
|
>>> LieDerivative(e_x, fx)
|
||
|
1
|
||
|
>>> LieDerivative(e_x, e_x)
|
||
|
0
|
||
|
|
||
|
The Lie derivative of a tensor field by another tensor field is equal to
|
||
|
their commutator:
|
||
|
|
||
|
>>> LieDerivative(e_x, e_rho)
|
||
|
Commutator(e_x, e_rho)
|
||
|
>>> LieDerivative(e_x + e_y, fx)
|
||
|
1
|
||
|
|
||
|
>>> tp = TensorProduct(dx, dy)
|
||
|
>>> LieDerivative(e_x, tp)
|
||
|
LieDerivative(e_x, TensorProduct(dx, dy))
|
||
|
>>> LieDerivative(e_x, tp)
|
||
|
LieDerivative(e_x, TensorProduct(dx, dy))
|
||
|
|
||
|
"""
|
||
|
def __new__(cls, v_field, expr):
|
||
|
expr_form_ord = covariant_order(expr)
|
||
|
if contravariant_order(v_field) != 1 or covariant_order(v_field):
|
||
|
raise ValueError('Lie derivatives are defined only with respect to'
|
||
|
' vector fields. The supplied argument was not a '
|
||
|
'vector field.')
|
||
|
if expr_form_ord > 0:
|
||
|
obj = super().__new__(cls, v_field, expr)
|
||
|
# deprecated assignments
|
||
|
obj._v_field = v_field
|
||
|
obj._expr = expr
|
||
|
return obj
|
||
|
if expr.atoms(BaseVectorField):
|
||
|
return Commutator(v_field, expr)
|
||
|
else:
|
||
|
return v_field.rcall(expr)
|
||
|
|
||
|
@property
|
||
|
def v_field(self):
|
||
|
return self.args[0]
|
||
|
|
||
|
@property
|
||
|
def expr(self):
|
||
|
return self.args[1]
|
||
|
|
||
|
def __call__(self, *args):
|
||
|
v = self.v_field
|
||
|
expr = self.expr
|
||
|
lead_term = v(expr(*args))
|
||
|
rest = Add(*[Mul(*args[:i] + (Commutator(v, args[i]),) + args[i + 1:])
|
||
|
for i in range(len(args))])
|
||
|
return lead_term - rest
|
||
|
|
||
|
|
||
|
class BaseCovarDerivativeOp(Expr):
|
||
|
"""Covariant derivative operator with respect to a base vector.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2_r
|
||
|
>>> from sympy.diffgeom import BaseCovarDerivativeOp
|
||
|
>>> from sympy.diffgeom import metric_to_Christoffel_2nd, TensorProduct
|
||
|
|
||
|
>>> TP = TensorProduct
|
||
|
>>> fx, fy = R2_r.base_scalars()
|
||
|
>>> e_x, e_y = R2_r.base_vectors()
|
||
|
>>> dx, dy = R2_r.base_oneforms()
|
||
|
|
||
|
>>> ch = metric_to_Christoffel_2nd(TP(dx, dx) + TP(dy, dy))
|
||
|
>>> ch
|
||
|
[[[0, 0], [0, 0]], [[0, 0], [0, 0]]]
|
||
|
>>> cvd = BaseCovarDerivativeOp(R2_r, 0, ch)
|
||
|
>>> cvd(fx)
|
||
|
1
|
||
|
>>> cvd(fx*e_x)
|
||
|
e_x
|
||
|
"""
|
||
|
|
||
|
def __new__(cls, coord_sys, index, christoffel):
|
||
|
index = _sympify(index)
|
||
|
christoffel = ImmutableDenseNDimArray(christoffel)
|
||
|
obj = super().__new__(cls, coord_sys, index, christoffel)
|
||
|
# deprecated assignments
|
||
|
obj._coord_sys = coord_sys
|
||
|
obj._index = index
|
||
|
obj._christoffel = christoffel
|
||
|
return obj
|
||
|
|
||
|
@property
|
||
|
def coord_sys(self):
|
||
|
return self.args[0]
|
||
|
|
||
|
@property
|
||
|
def index(self):
|
||
|
return self.args[1]
|
||
|
|
||
|
@property
|
||
|
def christoffel(self):
|
||
|
return self.args[2]
|
||
|
|
||
|
def __call__(self, field):
|
||
|
"""Apply on a scalar field.
|
||
|
|
||
|
The action of a vector field on a scalar field is a directional
|
||
|
differentiation.
|
||
|
If the argument is not a scalar field the behaviour is undefined.
|
||
|
"""
|
||
|
if covariant_order(field) != 0:
|
||
|
raise NotImplementedError()
|
||
|
|
||
|
field = vectors_in_basis(field, self._coord_sys)
|
||
|
|
||
|
wrt_vector = self._coord_sys.base_vector(self._index)
|
||
|
wrt_scalar = self._coord_sys.coord_function(self._index)
|
||
|
vectors = list(field.atoms(BaseVectorField))
|
||
|
|
||
|
# First step: replace all vectors with something susceptible to
|
||
|
# derivation and do the derivation
|
||
|
# TODO: you need a real dummy function for the next line
|
||
|
d_funcs = [Function('_#_%s' % i)(wrt_scalar) for i,
|
||
|
b in enumerate(vectors)]
|
||
|
d_result = field.subs(list(zip(vectors, d_funcs)))
|
||
|
d_result = wrt_vector(d_result)
|
||
|
|
||
|
# Second step: backsubstitute the vectors in
|
||
|
d_result = d_result.subs(list(zip(d_funcs, vectors)))
|
||
|
|
||
|
# Third step: evaluate the derivatives of the vectors
|
||
|
derivs = []
|
||
|
for v in vectors:
|
||
|
d = Add(*[(self._christoffel[k, wrt_vector._index, v._index]
|
||
|
*v._coord_sys.base_vector(k))
|
||
|
for k in range(v._coord_sys.dim)])
|
||
|
derivs.append(d)
|
||
|
to_subs = [wrt_vector(d) for d in d_funcs]
|
||
|
# XXX: This substitution can fail when there are Dummy symbols and the
|
||
|
# cache is disabled: https://github.com/sympy/sympy/issues/17794
|
||
|
result = d_result.subs(list(zip(to_subs, derivs)))
|
||
|
|
||
|
# Remove the dummies
|
||
|
result = result.subs(list(zip(d_funcs, vectors)))
|
||
|
return result.doit()
|
||
|
|
||
|
|
||
|
class CovarDerivativeOp(Expr):
|
||
|
"""Covariant derivative operator.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2_r
|
||
|
>>> from sympy.diffgeom import CovarDerivativeOp
|
||
|
>>> from sympy.diffgeom import metric_to_Christoffel_2nd, TensorProduct
|
||
|
>>> TP = TensorProduct
|
||
|
>>> fx, fy = R2_r.base_scalars()
|
||
|
>>> e_x, e_y = R2_r.base_vectors()
|
||
|
>>> dx, dy = R2_r.base_oneforms()
|
||
|
>>> ch = metric_to_Christoffel_2nd(TP(dx, dx) + TP(dy, dy))
|
||
|
|
||
|
>>> ch
|
||
|
[[[0, 0], [0, 0]], [[0, 0], [0, 0]]]
|
||
|
>>> cvd = CovarDerivativeOp(fx*e_x, ch)
|
||
|
>>> cvd(fx)
|
||
|
x
|
||
|
>>> cvd(fx*e_x)
|
||
|
x*e_x
|
||
|
|
||
|
"""
|
||
|
|
||
|
def __new__(cls, wrt, christoffel):
|
||
|
if len({v._coord_sys for v in wrt.atoms(BaseVectorField)}) > 1:
|
||
|
raise NotImplementedError()
|
||
|
if contravariant_order(wrt) != 1 or covariant_order(wrt):
|
||
|
raise ValueError('Covariant derivatives are defined only with '
|
||
|
'respect to vector fields. The supplied argument '
|
||
|
'was not a vector field.')
|
||
|
christoffel = ImmutableDenseNDimArray(christoffel)
|
||
|
obj = super().__new__(cls, wrt, christoffel)
|
||
|
# deprecated assignments
|
||
|
obj._wrt = wrt
|
||
|
obj._christoffel = christoffel
|
||
|
return obj
|
||
|
|
||
|
@property
|
||
|
def wrt(self):
|
||
|
return self.args[0]
|
||
|
|
||
|
@property
|
||
|
def christoffel(self):
|
||
|
return self.args[1]
|
||
|
|
||
|
def __call__(self, field):
|
||
|
vectors = list(self._wrt.atoms(BaseVectorField))
|
||
|
base_ops = [BaseCovarDerivativeOp(v._coord_sys, v._index, self._christoffel)
|
||
|
for v in vectors]
|
||
|
return self._wrt.subs(list(zip(vectors, base_ops))).rcall(field)
|
||
|
|
||
|
|
||
|
###############################################################################
|
||
|
# Integral curves on vector fields
|
||
|
###############################################################################
|
||
|
def intcurve_series(vector_field, param, start_point, n=6, coord_sys=None, coeffs=False):
|
||
|
r"""Return the series expansion for an integral curve of the field.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Integral curve is a function `\gamma` taking a parameter in `R` to a point
|
||
|
in the manifold. It verifies the equation:
|
||
|
|
||
|
`V(f)\big(\gamma(t)\big) = \frac{d}{dt}f\big(\gamma(t)\big)`
|
||
|
|
||
|
where the given ``vector_field`` is denoted as `V`. This holds for any
|
||
|
value `t` for the parameter and any scalar field `f`.
|
||
|
|
||
|
This equation can also be decomposed of a basis of coordinate functions
|
||
|
`V(f_i)\big(\gamma(t)\big) = \frac{d}{dt}f_i\big(\gamma(t)\big) \quad \forall i`
|
||
|
|
||
|
This function returns a series expansion of `\gamma(t)` in terms of the
|
||
|
coordinate system ``coord_sys``. The equations and expansions are necessarily
|
||
|
done in coordinate-system-dependent way as there is no other way to
|
||
|
represent movement between points on the manifold (i.e. there is no such
|
||
|
thing as a difference of points for a general manifold).
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
vector_field
|
||
|
the vector field for which an integral curve will be given
|
||
|
|
||
|
param
|
||
|
the argument of the function `\gamma` from R to the curve
|
||
|
|
||
|
start_point
|
||
|
the point which corresponds to `\gamma(0)`
|
||
|
|
||
|
n
|
||
|
the order to which to expand
|
||
|
|
||
|
coord_sys
|
||
|
the coordinate system in which to expand
|
||
|
coeffs (default False) - if True return a list of elements of the expansion
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
Use the predefined R2 manifold:
|
||
|
|
||
|
>>> from sympy.abc import t, x, y
|
||
|
>>> from sympy.diffgeom.rn import R2_p, R2_r
|
||
|
>>> from sympy.diffgeom import intcurve_series
|
||
|
|
||
|
Specify a starting point and a vector field:
|
||
|
|
||
|
>>> start_point = R2_r.point([x, y])
|
||
|
>>> vector_field = R2_r.e_x
|
||
|
|
||
|
Calculate the series:
|
||
|
|
||
|
>>> intcurve_series(vector_field, t, start_point, n=3)
|
||
|
Matrix([
|
||
|
[t + x],
|
||
|
[ y]])
|
||
|
|
||
|
Or get the elements of the expansion in a list:
|
||
|
|
||
|
>>> series = intcurve_series(vector_field, t, start_point, n=3, coeffs=True)
|
||
|
>>> series[0]
|
||
|
Matrix([
|
||
|
[x],
|
||
|
[y]])
|
||
|
>>> series[1]
|
||
|
Matrix([
|
||
|
[t],
|
||
|
[0]])
|
||
|
>>> series[2]
|
||
|
Matrix([
|
||
|
[0],
|
||
|
[0]])
|
||
|
|
||
|
The series in the polar coordinate system:
|
||
|
|
||
|
>>> series = intcurve_series(vector_field, t, start_point,
|
||
|
... n=3, coord_sys=R2_p, coeffs=True)
|
||
|
>>> series[0]
|
||
|
Matrix([
|
||
|
[sqrt(x**2 + y**2)],
|
||
|
[ atan2(y, x)]])
|
||
|
>>> series[1]
|
||
|
Matrix([
|
||
|
[t*x/sqrt(x**2 + y**2)],
|
||
|
[ -t*y/(x**2 + y**2)]])
|
||
|
>>> series[2]
|
||
|
Matrix([
|
||
|
[t**2*(-x**2/(x**2 + y**2)**(3/2) + 1/sqrt(x**2 + y**2))/2],
|
||
|
[ t**2*x*y/(x**2 + y**2)**2]])
|
||
|
|
||
|
See Also
|
||
|
========
|
||
|
|
||
|
intcurve_diffequ
|
||
|
|
||
|
"""
|
||
|
if contravariant_order(vector_field) != 1 or covariant_order(vector_field):
|
||
|
raise ValueError('The supplied field was not a vector field.')
|
||
|
|
||
|
def iter_vfield(scalar_field, i):
|
||
|
"""Return ``vector_field`` called `i` times on ``scalar_field``."""
|
||
|
return reduce(lambda s, v: v.rcall(s), [vector_field, ]*i, scalar_field)
|
||
|
|
||
|
def taylor_terms_per_coord(coord_function):
|
||
|
"""Return the series for one of the coordinates."""
|
||
|
return [param**i*iter_vfield(coord_function, i).rcall(start_point)/factorial(i)
|
||
|
for i in range(n)]
|
||
|
coord_sys = coord_sys if coord_sys else start_point._coord_sys
|
||
|
coord_functions = coord_sys.coord_functions()
|
||
|
taylor_terms = [taylor_terms_per_coord(f) for f in coord_functions]
|
||
|
if coeffs:
|
||
|
return [Matrix(t) for t in zip(*taylor_terms)]
|
||
|
else:
|
||
|
return Matrix([sum(c) for c in taylor_terms])
|
||
|
|
||
|
|
||
|
def intcurve_diffequ(vector_field, param, start_point, coord_sys=None):
|
||
|
r"""Return the differential equation for an integral curve of the field.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Integral curve is a function `\gamma` taking a parameter in `R` to a point
|
||
|
in the manifold. It verifies the equation:
|
||
|
|
||
|
`V(f)\big(\gamma(t)\big) = \frac{d}{dt}f\big(\gamma(t)\big)`
|
||
|
|
||
|
where the given ``vector_field`` is denoted as `V`. This holds for any
|
||
|
value `t` for the parameter and any scalar field `f`.
|
||
|
|
||
|
This function returns the differential equation of `\gamma(t)` in terms of the
|
||
|
coordinate system ``coord_sys``. The equations and expansions are necessarily
|
||
|
done in coordinate-system-dependent way as there is no other way to
|
||
|
represent movement between points on the manifold (i.e. there is no such
|
||
|
thing as a difference of points for a general manifold).
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
vector_field
|
||
|
the vector field for which an integral curve will be given
|
||
|
|
||
|
param
|
||
|
the argument of the function `\gamma` from R to the curve
|
||
|
|
||
|
start_point
|
||
|
the point which corresponds to `\gamma(0)`
|
||
|
|
||
|
coord_sys
|
||
|
the coordinate system in which to give the equations
|
||
|
|
||
|
Returns
|
||
|
=======
|
||
|
|
||
|
a tuple of (equations, initial conditions)
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
Use the predefined R2 manifold:
|
||
|
|
||
|
>>> from sympy.abc import t
|
||
|
>>> from sympy.diffgeom.rn import R2, R2_p, R2_r
|
||
|
>>> from sympy.diffgeom import intcurve_diffequ
|
||
|
|
||
|
Specify a starting point and a vector field:
|
||
|
|
||
|
>>> start_point = R2_r.point([0, 1])
|
||
|
>>> vector_field = -R2.y*R2.e_x + R2.x*R2.e_y
|
||
|
|
||
|
Get the equation:
|
||
|
|
||
|
>>> equations, init_cond = intcurve_diffequ(vector_field, t, start_point)
|
||
|
>>> equations
|
||
|
[f_1(t) + Derivative(f_0(t), t), -f_0(t) + Derivative(f_1(t), t)]
|
||
|
>>> init_cond
|
||
|
[f_0(0), f_1(0) - 1]
|
||
|
|
||
|
The series in the polar coordinate system:
|
||
|
|
||
|
>>> equations, init_cond = intcurve_diffequ(vector_field, t, start_point, R2_p)
|
||
|
>>> equations
|
||
|
[Derivative(f_0(t), t), Derivative(f_1(t), t) - 1]
|
||
|
>>> init_cond
|
||
|
[f_0(0) - 1, f_1(0) - pi/2]
|
||
|
|
||
|
See Also
|
||
|
========
|
||
|
|
||
|
intcurve_series
|
||
|
|
||
|
"""
|
||
|
if contravariant_order(vector_field) != 1 or covariant_order(vector_field):
|
||
|
raise ValueError('The supplied field was not a vector field.')
|
||
|
coord_sys = coord_sys if coord_sys else start_point._coord_sys
|
||
|
gammas = [Function('f_%d' % i)(param) for i in range(
|
||
|
start_point._coord_sys.dim)]
|
||
|
arbitrary_p = Point(coord_sys, gammas)
|
||
|
coord_functions = coord_sys.coord_functions()
|
||
|
equations = [simplify(diff(cf.rcall(arbitrary_p), param) - vector_field.rcall(cf).rcall(arbitrary_p))
|
||
|
for cf in coord_functions]
|
||
|
init_cond = [simplify(cf.rcall(arbitrary_p).subs(param, 0) - cf.rcall(start_point))
|
||
|
for cf in coord_functions]
|
||
|
return equations, init_cond
|
||
|
|
||
|
|
||
|
###############################################################################
|
||
|
# Helpers
|
||
|
###############################################################################
|
||
|
def dummyfy(args, exprs):
|
||
|
# TODO Is this a good idea?
|
||
|
d_args = Matrix([s.as_dummy() for s in args])
|
||
|
reps = dict(zip(args, d_args))
|
||
|
d_exprs = Matrix([_sympify(expr).subs(reps) for expr in exprs])
|
||
|
return d_args, d_exprs
|
||
|
|
||
|
###############################################################################
|
||
|
# Helpers
|
||
|
###############################################################################
|
||
|
def contravariant_order(expr, _strict=False):
|
||
|
"""Return the contravariant order of an expression.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom import contravariant_order
|
||
|
>>> from sympy.diffgeom.rn import R2
|
||
|
>>> from sympy.abc import a
|
||
|
|
||
|
>>> contravariant_order(a)
|
||
|
0
|
||
|
>>> contravariant_order(a*R2.x + 2)
|
||
|
0
|
||
|
>>> contravariant_order(a*R2.x*R2.e_y + R2.e_x)
|
||
|
1
|
||
|
|
||
|
"""
|
||
|
# TODO move some of this to class methods.
|
||
|
# TODO rewrite using the .as_blah_blah methods
|
||
|
if isinstance(expr, Add):
|
||
|
orders = [contravariant_order(e) for e in expr.args]
|
||
|
if len(set(orders)) != 1:
|
||
|
raise ValueError('Misformed expression containing contravariant fields of varying order.')
|
||
|
return orders[0]
|
||
|
elif isinstance(expr, Mul):
|
||
|
orders = [contravariant_order(e) for e in expr.args]
|
||
|
not_zero = [o for o in orders if o != 0]
|
||
|
if len(not_zero) > 1:
|
||
|
raise ValueError('Misformed expression containing multiplication between vectors.')
|
||
|
return 0 if not not_zero else not_zero[0]
|
||
|
elif isinstance(expr, Pow):
|
||
|
if covariant_order(expr.base) or covariant_order(expr.exp):
|
||
|
raise ValueError(
|
||
|
'Misformed expression containing a power of a vector.')
|
||
|
return 0
|
||
|
elif isinstance(expr, BaseVectorField):
|
||
|
return 1
|
||
|
elif isinstance(expr, TensorProduct):
|
||
|
return sum(contravariant_order(a) for a in expr.args)
|
||
|
elif not _strict or expr.atoms(BaseScalarField):
|
||
|
return 0
|
||
|
else: # If it does not contain anything related to the diffgeom module and it is _strict
|
||
|
return -1
|
||
|
|
||
|
|
||
|
def covariant_order(expr, _strict=False):
|
||
|
"""Return the covariant order of an expression.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom import covariant_order
|
||
|
>>> from sympy.diffgeom.rn import R2
|
||
|
>>> from sympy.abc import a
|
||
|
|
||
|
>>> covariant_order(a)
|
||
|
0
|
||
|
>>> covariant_order(a*R2.x + 2)
|
||
|
0
|
||
|
>>> covariant_order(a*R2.x*R2.dy + R2.dx)
|
||
|
1
|
||
|
|
||
|
"""
|
||
|
# TODO move some of this to class methods.
|
||
|
# TODO rewrite using the .as_blah_blah methods
|
||
|
if isinstance(expr, Add):
|
||
|
orders = [covariant_order(e) for e in expr.args]
|
||
|
if len(set(orders)) != 1:
|
||
|
raise ValueError('Misformed expression containing form fields of varying order.')
|
||
|
return orders[0]
|
||
|
elif isinstance(expr, Mul):
|
||
|
orders = [covariant_order(e) for e in expr.args]
|
||
|
not_zero = [o for o in orders if o != 0]
|
||
|
if len(not_zero) > 1:
|
||
|
raise ValueError('Misformed expression containing multiplication between forms.')
|
||
|
return 0 if not not_zero else not_zero[0]
|
||
|
elif isinstance(expr, Pow):
|
||
|
if covariant_order(expr.base) or covariant_order(expr.exp):
|
||
|
raise ValueError(
|
||
|
'Misformed expression containing a power of a form.')
|
||
|
return 0
|
||
|
elif isinstance(expr, Differential):
|
||
|
return covariant_order(*expr.args) + 1
|
||
|
elif isinstance(expr, TensorProduct):
|
||
|
return sum(covariant_order(a) for a in expr.args)
|
||
|
elif not _strict or expr.atoms(BaseScalarField):
|
||
|
return 0
|
||
|
else: # If it does not contain anything related to the diffgeom module and it is _strict
|
||
|
return -1
|
||
|
|
||
|
|
||
|
###############################################################################
|
||
|
# Coordinate transformation functions
|
||
|
###############################################################################
|
||
|
def vectors_in_basis(expr, to_sys):
|
||
|
"""Transform all base vectors in base vectors of a specified coord basis.
|
||
|
While the new base vectors are in the new coordinate system basis, any
|
||
|
coefficients are kept in the old system.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom import vectors_in_basis
|
||
|
>>> from sympy.diffgeom.rn import R2_r, R2_p
|
||
|
|
||
|
>>> vectors_in_basis(R2_r.e_x, R2_p)
|
||
|
-y*e_theta/(x**2 + y**2) + x*e_rho/sqrt(x**2 + y**2)
|
||
|
>>> vectors_in_basis(R2_p.e_r, R2_r)
|
||
|
sin(theta)*e_y + cos(theta)*e_x
|
||
|
|
||
|
"""
|
||
|
vectors = list(expr.atoms(BaseVectorField))
|
||
|
new_vectors = []
|
||
|
for v in vectors:
|
||
|
cs = v._coord_sys
|
||
|
jac = cs.jacobian(to_sys, cs.coord_functions())
|
||
|
new = (jac.T*Matrix(to_sys.base_vectors()))[v._index]
|
||
|
new_vectors.append(new)
|
||
|
return expr.subs(list(zip(vectors, new_vectors)))
|
||
|
|
||
|
|
||
|
###############################################################################
|
||
|
# Coordinate-dependent functions
|
||
|
###############################################################################
|
||
|
def twoform_to_matrix(expr):
|
||
|
"""Return the matrix representing the twoform.
|
||
|
|
||
|
For the twoform `w` return the matrix `M` such that `M[i,j]=w(e_i, e_j)`,
|
||
|
where `e_i` is the i-th base vector field for the coordinate system in
|
||
|
which the expression of `w` is given.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2
|
||
|
>>> from sympy.diffgeom import twoform_to_matrix, TensorProduct
|
||
|
>>> TP = TensorProduct
|
||
|
|
||
|
>>> twoform_to_matrix(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy))
|
||
|
Matrix([
|
||
|
[1, 0],
|
||
|
[0, 1]])
|
||
|
>>> twoform_to_matrix(R2.x*TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy))
|
||
|
Matrix([
|
||
|
[x, 0],
|
||
|
[0, 1]])
|
||
|
>>> twoform_to_matrix(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy) - TP(R2.dx, R2.dy)/2)
|
||
|
Matrix([
|
||
|
[ 1, 0],
|
||
|
[-1/2, 1]])
|
||
|
|
||
|
"""
|
||
|
if covariant_order(expr) != 2 or contravariant_order(expr):
|
||
|
raise ValueError('The input expression is not a two-form.')
|
||
|
coord_sys = _find_coords(expr)
|
||
|
if len(coord_sys) != 1:
|
||
|
raise ValueError('The input expression concerns more than one '
|
||
|
'coordinate systems, hence there is no unambiguous '
|
||
|
'way to choose a coordinate system for the matrix.')
|
||
|
coord_sys = coord_sys.pop()
|
||
|
vectors = coord_sys.base_vectors()
|
||
|
expr = expr.expand()
|
||
|
matrix_content = [[expr.rcall(v1, v2) for v1 in vectors]
|
||
|
for v2 in vectors]
|
||
|
return Matrix(matrix_content)
|
||
|
|
||
|
|
||
|
def metric_to_Christoffel_1st(expr):
|
||
|
"""Return the nested list of Christoffel symbols for the given metric.
|
||
|
This returns the Christoffel symbol of first kind that represents the
|
||
|
Levi-Civita connection for the given metric.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2
|
||
|
>>> from sympy.diffgeom import metric_to_Christoffel_1st, TensorProduct
|
||
|
>>> TP = TensorProduct
|
||
|
|
||
|
>>> metric_to_Christoffel_1st(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy))
|
||
|
[[[0, 0], [0, 0]], [[0, 0], [0, 0]]]
|
||
|
>>> metric_to_Christoffel_1st(R2.x*TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy))
|
||
|
[[[1/2, 0], [0, 0]], [[0, 0], [0, 0]]]
|
||
|
|
||
|
"""
|
||
|
matrix = twoform_to_matrix(expr)
|
||
|
if not matrix.is_symmetric():
|
||
|
raise ValueError(
|
||
|
'The two-form representing the metric is not symmetric.')
|
||
|
coord_sys = _find_coords(expr).pop()
|
||
|
deriv_matrices = [matrix.applyfunc(d) for d in coord_sys.base_vectors()]
|
||
|
indices = list(range(coord_sys.dim))
|
||
|
christoffel = [[[(deriv_matrices[k][i, j] + deriv_matrices[j][i, k] - deriv_matrices[i][j, k])/2
|
||
|
for k in indices]
|
||
|
for j in indices]
|
||
|
for i in indices]
|
||
|
return ImmutableDenseNDimArray(christoffel)
|
||
|
|
||
|
|
||
|
def metric_to_Christoffel_2nd(expr):
|
||
|
"""Return the nested list of Christoffel symbols for the given metric.
|
||
|
This returns the Christoffel symbol of second kind that represents the
|
||
|
Levi-Civita connection for the given metric.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.diffgeom.rn import R2
|
||
|
>>> from sympy.diffgeom import metric_to_Christoffel_2nd, TensorProduct
|
||
|
>>> TP = TensorProduct
|
||
|
|
||
|
>>> metric_to_Christoffel_2nd(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy))
|
||
|
[[[0, 0], [0, 0]], [[0, 0], [0, 0]]]
|
||
|
>>> metric_to_Christoffel_2nd(R2.x*TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy))
|
||
|
[[[1/(2*x), 0], [0, 0]], [[0, 0], [0, 0]]]
|
||
|
|
||
|
"""
|
||
|
ch_1st = metric_to_Christoffel_1st(expr)
|
||
|
coord_sys = _find_coords(expr).pop()
|
||
|
indices = list(range(coord_sys.dim))
|
||
|
# XXX workaround, inverting a matrix does not work if it contains non
|
||
|
# symbols
|
||
|
#matrix = twoform_to_matrix(expr).inv()
|
||
|
matrix = twoform_to_matrix(expr)
|
||
|
s_fields = set()
|
||
|
for e in matrix:
|
||
|
s_fields.update(e.atoms(BaseScalarField))
|
||
|
s_fields = list(s_fields)
|
||
|
dums = coord_sys.symbols
|
||
|
matrix = matrix.subs(list(zip(s_fields, dums))).inv().subs(list(zip(dums, s_fields)))
|
||
|
# XXX end of workaround
|
||
|
christoffel = [[[Add(*[matrix[i, l]*ch_1st[l, j, k] for l in indices])
|
||
|
for k in indices]
|
||
|
for j in indices]
|
||
|
for i in indices]
|
||
|
return ImmutableDenseNDimArray(christoffel)
|
||
|
|
||
|
|
||
|
def metric_to_Riemann_components(expr):
|
||
|
"""Return the components of the Riemann tensor expressed in a given basis.
|
||
|
|
||
|
Given a metric it calculates the components of the Riemann tensor in the
|
||
|
canonical basis of the coordinate system in which the metric expression is
|
||
|
given.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import exp
|
||
|
>>> from sympy.diffgeom.rn import R2
|
||
|
>>> from sympy.diffgeom import metric_to_Riemann_components, TensorProduct
|
||
|
>>> TP = TensorProduct
|
||
|
|
||
|
>>> metric_to_Riemann_components(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy))
|
||
|
[[[[0, 0], [0, 0]], [[0, 0], [0, 0]]], [[[0, 0], [0, 0]], [[0, 0], [0, 0]]]]
|
||
|
>>> non_trivial_metric = exp(2*R2.r)*TP(R2.dr, R2.dr) + \
|
||
|
R2.r**2*TP(R2.dtheta, R2.dtheta)
|
||
|
>>> non_trivial_metric
|
||
|
exp(2*rho)*TensorProduct(drho, drho) + rho**2*TensorProduct(dtheta, dtheta)
|
||
|
>>> riemann = metric_to_Riemann_components(non_trivial_metric)
|
||
|
>>> riemann[0, :, :, :]
|
||
|
[[[0, 0], [0, 0]], [[0, exp(-2*rho)*rho], [-exp(-2*rho)*rho, 0]]]
|
||
|
>>> riemann[1, :, :, :]
|
||
|
[[[0, -1/rho], [1/rho, 0]], [[0, 0], [0, 0]]]
|
||
|
|
||
|
"""
|
||
|
ch_2nd = metric_to_Christoffel_2nd(expr)
|
||
|
coord_sys = _find_coords(expr).pop()
|
||
|
indices = list(range(coord_sys.dim))
|
||
|
deriv_ch = [[[[d(ch_2nd[i, j, k])
|
||
|
for d in coord_sys.base_vectors()]
|
||
|
for k in indices]
|
||
|
for j in indices]
|
||
|
for i in indices]
|
||
|
riemann_a = [[[[deriv_ch[rho][sig][nu][mu] - deriv_ch[rho][sig][mu][nu]
|
||
|
for nu in indices]
|
||
|
for mu in indices]
|
||
|
for sig in indices]
|
||
|
for rho in indices]
|
||
|
riemann_b = [[[[Add(*[ch_2nd[rho, l, mu]*ch_2nd[l, sig, nu] - ch_2nd[rho, l, nu]*ch_2nd[l, sig, mu] for l in indices])
|
||
|
for nu in indices]
|
||
|
for mu in indices]
|
||
|
for sig in indices]
|
||
|
for rho in indices]
|
||
|
riemann = [[[[riemann_a[rho][sig][mu][nu] + riemann_b[rho][sig][mu][nu]
|
||
|
for nu in indices]
|
||
|
for mu in indices]
|
||
|
for sig in indices]
|
||
|
for rho in indices]
|
||
|
return ImmutableDenseNDimArray(riemann)
|
||
|
|
||
|
|
||
|
def metric_to_Ricci_components(expr):
|
||
|
|
||
|
"""Return the components of the Ricci tensor expressed in a given basis.
|
||
|
|
||
|
Given a metric it calculates the components of the Ricci tensor in the
|
||
|
canonical basis of the coordinate system in which the metric expression is
|
||
|
given.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import exp
|
||
|
>>> from sympy.diffgeom.rn import R2
|
||
|
>>> from sympy.diffgeom import metric_to_Ricci_components, TensorProduct
|
||
|
>>> TP = TensorProduct
|
||
|
|
||
|
>>> metric_to_Ricci_components(TP(R2.dx, R2.dx) + TP(R2.dy, R2.dy))
|
||
|
[[0, 0], [0, 0]]
|
||
|
>>> non_trivial_metric = exp(2*R2.r)*TP(R2.dr, R2.dr) + \
|
||
|
R2.r**2*TP(R2.dtheta, R2.dtheta)
|
||
|
>>> non_trivial_metric
|
||
|
exp(2*rho)*TensorProduct(drho, drho) + rho**2*TensorProduct(dtheta, dtheta)
|
||
|
>>> metric_to_Ricci_components(non_trivial_metric)
|
||
|
[[1/rho, 0], [0, exp(-2*rho)*rho]]
|
||
|
|
||
|
"""
|
||
|
riemann = metric_to_Riemann_components(expr)
|
||
|
coord_sys = _find_coords(expr).pop()
|
||
|
indices = list(range(coord_sys.dim))
|
||
|
ricci = [[Add(*[riemann[k, i, k, j] for k in indices])
|
||
|
for j in indices]
|
||
|
for i in indices]
|
||
|
return ImmutableDenseNDimArray(ricci)
|
||
|
|
||
|
###############################################################################
|
||
|
# Classes for deprecation
|
||
|
###############################################################################
|
||
|
|
||
|
class _deprecated_container:
|
||
|
# This class gives deprecation warning.
|
||
|
# When deprecated features are completely deleted, this should be removed as well.
|
||
|
# See https://github.com/sympy/sympy/pull/19368
|
||
|
def __init__(self, message, data):
|
||
|
super().__init__(data)
|
||
|
self.message = message
|
||
|
|
||
|
def warn(self):
|
||
|
sympy_deprecation_warning(
|
||
|
self.message,
|
||
|
deprecated_since_version="1.7",
|
||
|
active_deprecations_target="deprecated-diffgeom-mutable",
|
||
|
stacklevel=4
|
||
|
)
|
||
|
|
||
|
def __iter__(self):
|
||
|
self.warn()
|
||
|
return super().__iter__()
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
self.warn()
|
||
|
return super().__getitem__(key)
|
||
|
|
||
|
def __contains__(self, key):
|
||
|
self.warn()
|
||
|
return super().__contains__(key)
|
||
|
|
||
|
|
||
|
class _deprecated_list(_deprecated_container, list):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class _deprecated_dict(_deprecated_container, dict):
|
||
|
pass
|
||
|
|
||
|
|
||
|
# Import at end to avoid cyclic imports
|
||
|
from sympy.simplify.simplify import simplify
|