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.
477 lines
17 KiB
477 lines
17 KiB
5 months ago
|
"""
|
||
|
Finite difference weights
|
||
|
=========================
|
||
|
|
||
|
This module implements an algorithm for efficient generation of finite
|
||
|
difference weights for ordinary differentials of functions for
|
||
|
derivatives from 0 (interpolation) up to arbitrary order.
|
||
|
|
||
|
The core algorithm is provided in the finite difference weight generating
|
||
|
function (``finite_diff_weights``), and two convenience functions are provided
|
||
|
for:
|
||
|
|
||
|
- estimating a derivative (or interpolate) directly from a series of points
|
||
|
is also provided (``apply_finite_diff``).
|
||
|
- differentiating by using finite difference approximations
|
||
|
(``differentiate_finite``).
|
||
|
|
||
|
"""
|
||
|
|
||
|
from sympy.core.function import Derivative
|
||
|
from sympy.core.singleton import S
|
||
|
from sympy.core.function import Subs
|
||
|
from sympy.core.traversal import preorder_traversal
|
||
|
from sympy.utilities.exceptions import sympy_deprecation_warning
|
||
|
from sympy.utilities.iterables import iterable
|
||
|
|
||
|
|
||
|
|
||
|
def finite_diff_weights(order, x_list, x0=S.One):
|
||
|
"""
|
||
|
Calculates the finite difference weights for an arbitrarily spaced
|
||
|
one-dimensional grid (``x_list``) for derivatives at ``x0`` of order
|
||
|
0, 1, ..., up to ``order`` using a recursive formula. Order of accuracy
|
||
|
is at least ``len(x_list) - order``, if ``x_list`` is defined correctly.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
order: int
|
||
|
Up to what derivative order weights should be calculated.
|
||
|
0 corresponds to interpolation.
|
||
|
x_list: sequence
|
||
|
Sequence of (unique) values for the independent variable.
|
||
|
It is useful (but not necessary) to order ``x_list`` from
|
||
|
nearest to furthest from ``x0``; see examples below.
|
||
|
x0: Number or Symbol
|
||
|
Root or value of the independent variable for which the finite
|
||
|
difference weights should be generated. Default is ``S.One``.
|
||
|
|
||
|
Returns
|
||
|
=======
|
||
|
|
||
|
list
|
||
|
A list of sublists, each corresponding to coefficients for
|
||
|
increasing derivative order, and each containing lists of
|
||
|
coefficients for increasing subsets of x_list.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import finite_diff_weights, S
|
||
|
>>> res = finite_diff_weights(1, [-S(1)/2, S(1)/2, S(3)/2, S(5)/2], 0)
|
||
|
>>> res
|
||
|
[[[1, 0, 0, 0],
|
||
|
[1/2, 1/2, 0, 0],
|
||
|
[3/8, 3/4, -1/8, 0],
|
||
|
[5/16, 15/16, -5/16, 1/16]],
|
||
|
[[0, 0, 0, 0],
|
||
|
[-1, 1, 0, 0],
|
||
|
[-1, 1, 0, 0],
|
||
|
[-23/24, 7/8, 1/8, -1/24]]]
|
||
|
>>> res[0][-1] # FD weights for 0th derivative, using full x_list
|
||
|
[5/16, 15/16, -5/16, 1/16]
|
||
|
>>> res[1][-1] # FD weights for 1st derivative
|
||
|
[-23/24, 7/8, 1/8, -1/24]
|
||
|
>>> res[1][-2] # FD weights for 1st derivative, using x_list[:-1]
|
||
|
[-1, 1, 0, 0]
|
||
|
>>> res[1][-1][0] # FD weight for 1st deriv. for x_list[0]
|
||
|
-23/24
|
||
|
>>> res[1][-1][1] # FD weight for 1st deriv. for x_list[1], etc.
|
||
|
7/8
|
||
|
|
||
|
Each sublist contains the most accurate formula at the end.
|
||
|
Note, that in the above example ``res[1][1]`` is the same as ``res[1][2]``.
|
||
|
Since res[1][2] has an order of accuracy of
|
||
|
``len(x_list[:3]) - order = 3 - 1 = 2``, the same is true for ``res[1][1]``!
|
||
|
|
||
|
>>> res = finite_diff_weights(1, [S(0), S(1), -S(1), S(2), -S(2)], 0)[1]
|
||
|
>>> res
|
||
|
[[0, 0, 0, 0, 0],
|
||
|
[-1, 1, 0, 0, 0],
|
||
|
[0, 1/2, -1/2, 0, 0],
|
||
|
[-1/2, 1, -1/3, -1/6, 0],
|
||
|
[0, 2/3, -2/3, -1/12, 1/12]]
|
||
|
>>> res[0] # no approximation possible, using x_list[0] only
|
||
|
[0, 0, 0, 0, 0]
|
||
|
>>> res[1] # classic forward step approximation
|
||
|
[-1, 1, 0, 0, 0]
|
||
|
>>> res[2] # classic centered approximation
|
||
|
[0, 1/2, -1/2, 0, 0]
|
||
|
>>> res[3:] # higher order approximations
|
||
|
[[-1/2, 1, -1/3, -1/6, 0], [0, 2/3, -2/3, -1/12, 1/12]]
|
||
|
|
||
|
Let us compare this to a differently defined ``x_list``. Pay attention to
|
||
|
``foo[i][k]`` corresponding to the gridpoint defined by ``x_list[k]``.
|
||
|
|
||
|
>>> foo = finite_diff_weights(1, [-S(2), -S(1), S(0), S(1), S(2)], 0)[1]
|
||
|
>>> foo
|
||
|
[[0, 0, 0, 0, 0],
|
||
|
[-1, 1, 0, 0, 0],
|
||
|
[1/2, -2, 3/2, 0, 0],
|
||
|
[1/6, -1, 1/2, 1/3, 0],
|
||
|
[1/12, -2/3, 0, 2/3, -1/12]]
|
||
|
>>> foo[1] # not the same and of lower accuracy as res[1]!
|
||
|
[-1, 1, 0, 0, 0]
|
||
|
>>> foo[2] # classic double backward step approximation
|
||
|
[1/2, -2, 3/2, 0, 0]
|
||
|
>>> foo[4] # the same as res[4]
|
||
|
[1/12, -2/3, 0, 2/3, -1/12]
|
||
|
|
||
|
Note that, unless you plan on using approximations based on subsets of
|
||
|
``x_list``, the order of gridpoints does not matter.
|
||
|
|
||
|
The capability to generate weights at arbitrary points can be
|
||
|
used e.g. to minimize Runge's phenomenon by using Chebyshev nodes:
|
||
|
|
||
|
>>> from sympy import cos, symbols, pi, simplify
|
||
|
>>> N, (h, x) = 4, symbols('h x')
|
||
|
>>> x_list = [x+h*cos(i*pi/(N)) for i in range(N,-1,-1)] # chebyshev nodes
|
||
|
>>> print(x_list)
|
||
|
[-h + x, -sqrt(2)*h/2 + x, x, sqrt(2)*h/2 + x, h + x]
|
||
|
>>> mycoeffs = finite_diff_weights(1, x_list, 0)[1][4]
|
||
|
>>> [simplify(c) for c in mycoeffs] #doctest: +NORMALIZE_WHITESPACE
|
||
|
[(h**3/2 + h**2*x - 3*h*x**2 - 4*x**3)/h**4,
|
||
|
(-sqrt(2)*h**3 - 4*h**2*x + 3*sqrt(2)*h*x**2 + 8*x**3)/h**4,
|
||
|
(6*h**2*x - 8*x**3)/h**4,
|
||
|
(sqrt(2)*h**3 - 4*h**2*x - 3*sqrt(2)*h*x**2 + 8*x**3)/h**4,
|
||
|
(-h**3/2 + h**2*x + 3*h*x**2 - 4*x**3)/h**4]
|
||
|
|
||
|
Notes
|
||
|
=====
|
||
|
|
||
|
If weights for a finite difference approximation of 3rd order
|
||
|
derivative is wanted, weights for 0th, 1st and 2nd order are
|
||
|
calculated "for free", so are formulae using subsets of ``x_list``.
|
||
|
This is something one can take advantage of to save computational cost.
|
||
|
Be aware that one should define ``x_list`` from nearest to furthest from
|
||
|
``x0``. If not, subsets of ``x_list`` will yield poorer approximations,
|
||
|
which might not grand an order of accuracy of ``len(x_list) - order``.
|
||
|
|
||
|
See also
|
||
|
========
|
||
|
|
||
|
sympy.calculus.finite_diff.apply_finite_diff
|
||
|
|
||
|
References
|
||
|
==========
|
||
|
|
||
|
.. [1] Generation of Finite Difference Formulas on Arbitrarily Spaced
|
||
|
Grids, Bengt Fornberg; Mathematics of computation; 51; 184;
|
||
|
(1988); 699-706; doi:10.1090/S0025-5718-1988-0935077-0
|
||
|
|
||
|
"""
|
||
|
# The notation below closely corresponds to the one used in the paper.
|
||
|
order = S(order)
|
||
|
if not order.is_number:
|
||
|
raise ValueError("Cannot handle symbolic order.")
|
||
|
if order < 0:
|
||
|
raise ValueError("Negative derivative order illegal.")
|
||
|
if int(order) != order:
|
||
|
raise ValueError("Non-integer order illegal")
|
||
|
M = order
|
||
|
N = len(x_list) - 1
|
||
|
delta = [[[0 for nu in range(N+1)] for n in range(N+1)] for
|
||
|
m in range(M+1)]
|
||
|
delta[0][0][0] = S.One
|
||
|
c1 = S.One
|
||
|
for n in range(1, N+1):
|
||
|
c2 = S.One
|
||
|
for nu in range(n):
|
||
|
c3 = x_list[n] - x_list[nu]
|
||
|
c2 = c2 * c3
|
||
|
if n <= M:
|
||
|
delta[n][n-1][nu] = 0
|
||
|
for m in range(min(n, M)+1):
|
||
|
delta[m][n][nu] = (x_list[n]-x0)*delta[m][n-1][nu] -\
|
||
|
m*delta[m-1][n-1][nu]
|
||
|
delta[m][n][nu] /= c3
|
||
|
for m in range(min(n, M)+1):
|
||
|
delta[m][n][n] = c1/c2*(m*delta[m-1][n-1][n-1] -
|
||
|
(x_list[n-1]-x0)*delta[m][n-1][n-1])
|
||
|
c1 = c2
|
||
|
return delta
|
||
|
|
||
|
|
||
|
def apply_finite_diff(order, x_list, y_list, x0=S.Zero):
|
||
|
"""
|
||
|
Calculates the finite difference approximation of
|
||
|
the derivative of requested order at ``x0`` from points
|
||
|
provided in ``x_list`` and ``y_list``.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
order: int
|
||
|
order of derivative to approximate. 0 corresponds to interpolation.
|
||
|
x_list: sequence
|
||
|
Sequence of (unique) values for the independent variable.
|
||
|
y_list: sequence
|
||
|
The function value at corresponding values for the independent
|
||
|
variable in x_list.
|
||
|
x0: Number or Symbol
|
||
|
At what value of the independent variable the derivative should be
|
||
|
evaluated. Defaults to 0.
|
||
|
|
||
|
Returns
|
||
|
=======
|
||
|
|
||
|
sympy.core.add.Add or sympy.core.numbers.Number
|
||
|
The finite difference expression approximating the requested
|
||
|
derivative order at ``x0``.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import apply_finite_diff
|
||
|
>>> cube = lambda arg: (1.0*arg)**3
|
||
|
>>> xlist = range(-3,3+1)
|
||
|
>>> apply_finite_diff(2, xlist, map(cube, xlist), 2) - 12 # doctest: +SKIP
|
||
|
-3.55271367880050e-15
|
||
|
|
||
|
we see that the example above only contain rounding errors.
|
||
|
apply_finite_diff can also be used on more abstract objects:
|
||
|
|
||
|
>>> from sympy import IndexedBase, Idx
|
||
|
>>> x, y = map(IndexedBase, 'xy')
|
||
|
>>> i = Idx('i')
|
||
|
>>> x_list, y_list = zip(*[(x[i+j], y[i+j]) for j in range(-1,2)])
|
||
|
>>> apply_finite_diff(1, x_list, y_list, x[i])
|
||
|
((x[i + 1] - x[i])/(-x[i - 1] + x[i]) - 1)*y[i]/(x[i + 1] - x[i]) -
|
||
|
(x[i + 1] - x[i])*y[i - 1]/((x[i + 1] - x[i - 1])*(-x[i - 1] + x[i])) +
|
||
|
(-x[i - 1] + x[i])*y[i + 1]/((x[i + 1] - x[i - 1])*(x[i + 1] - x[i]))
|
||
|
|
||
|
Notes
|
||
|
=====
|
||
|
|
||
|
Order = 0 corresponds to interpolation.
|
||
|
Only supply so many points you think makes sense
|
||
|
to around x0 when extracting the derivative (the function
|
||
|
need to be well behaved within that region). Also beware
|
||
|
of Runge's phenomenon.
|
||
|
|
||
|
See also
|
||
|
========
|
||
|
|
||
|
sympy.calculus.finite_diff.finite_diff_weights
|
||
|
|
||
|
References
|
||
|
==========
|
||
|
|
||
|
Fortran 90 implementation with Python interface for numerics: finitediff_
|
||
|
|
||
|
.. _finitediff: https://github.com/bjodah/finitediff
|
||
|
|
||
|
"""
|
||
|
|
||
|
# In the original paper the following holds for the notation:
|
||
|
# M = order
|
||
|
# N = len(x_list) - 1
|
||
|
|
||
|
N = len(x_list) - 1
|
||
|
if len(x_list) != len(y_list):
|
||
|
raise ValueError("x_list and y_list not equal in length.")
|
||
|
|
||
|
delta = finite_diff_weights(order, x_list, x0)
|
||
|
|
||
|
derivative = 0
|
||
|
for nu in range(len(x_list)):
|
||
|
derivative += delta[order][N][nu]*y_list[nu]
|
||
|
return derivative
|
||
|
|
||
|
|
||
|
def _as_finite_diff(derivative, points=1, x0=None, wrt=None):
|
||
|
"""
|
||
|
Returns an approximation of a derivative of a function in
|
||
|
the form of a finite difference formula. The expression is a
|
||
|
weighted sum of the function at a number of discrete values of
|
||
|
(one of) the independent variable(s).
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
derivative: a Derivative instance
|
||
|
|
||
|
points: sequence or coefficient, optional
|
||
|
If sequence: discrete values (length >= order+1) of the
|
||
|
independent variable used for generating the finite
|
||
|
difference weights.
|
||
|
If it is a coefficient, it will be used as the step-size
|
||
|
for generating an equidistant sequence of length order+1
|
||
|
centered around ``x0``. default: 1 (step-size 1)
|
||
|
|
||
|
x0: number or Symbol, optional
|
||
|
the value of the independent variable (``wrt``) at which the
|
||
|
derivative is to be approximated. Default: same as ``wrt``.
|
||
|
|
||
|
wrt: Symbol, optional
|
||
|
"with respect to" the variable for which the (partial)
|
||
|
derivative is to be approximated for. If not provided it
|
||
|
is required that the Derivative is ordinary. Default: ``None``.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import symbols, Function, exp, sqrt, Symbol
|
||
|
>>> from sympy.calculus.finite_diff import _as_finite_diff
|
||
|
>>> x, h = symbols('x h')
|
||
|
>>> f = Function('f')
|
||
|
>>> _as_finite_diff(f(x).diff(x))
|
||
|
-f(x - 1/2) + f(x + 1/2)
|
||
|
|
||
|
The default step size and number of points are 1 and ``order + 1``
|
||
|
respectively. We can change the step size by passing a symbol
|
||
|
as a parameter:
|
||
|
|
||
|
>>> _as_finite_diff(f(x).diff(x), h)
|
||
|
-f(-h/2 + x)/h + f(h/2 + x)/h
|
||
|
|
||
|
We can also specify the discretized values to be used in a sequence:
|
||
|
|
||
|
>>> _as_finite_diff(f(x).diff(x), [x, x+h, x+2*h])
|
||
|
-3*f(x)/(2*h) + 2*f(h + x)/h - f(2*h + x)/(2*h)
|
||
|
|
||
|
The algorithm is not restricted to use equidistant spacing, nor
|
||
|
do we need to make the approximation around ``x0``, but we can get
|
||
|
an expression estimating the derivative at an offset:
|
||
|
|
||
|
>>> e, sq2 = exp(1), sqrt(2)
|
||
|
>>> xl = [x-h, x+h, x+e*h]
|
||
|
>>> _as_finite_diff(f(x).diff(x, 1), xl, x+h*sq2)
|
||
|
2*h*((h + sqrt(2)*h)/(2*h) - (-sqrt(2)*h + h)/(2*h))*f(E*h + x)/((-h + E*h)*(h + E*h)) +
|
||
|
(-(-sqrt(2)*h + h)/(2*h) - (-sqrt(2)*h + E*h)/(2*h))*f(-h + x)/(h + E*h) +
|
||
|
(-(h + sqrt(2)*h)/(2*h) + (-sqrt(2)*h + E*h)/(2*h))*f(h + x)/(-h + E*h)
|
||
|
|
||
|
Partial derivatives are also supported:
|
||
|
|
||
|
>>> y = Symbol('y')
|
||
|
>>> d2fdxdy=f(x,y).diff(x,y)
|
||
|
>>> _as_finite_diff(d2fdxdy, wrt=x)
|
||
|
-Derivative(f(x - 1/2, y), y) + Derivative(f(x + 1/2, y), y)
|
||
|
|
||
|
See also
|
||
|
========
|
||
|
|
||
|
sympy.calculus.finite_diff.apply_finite_diff
|
||
|
sympy.calculus.finite_diff.finite_diff_weights
|
||
|
|
||
|
"""
|
||
|
if derivative.is_Derivative:
|
||
|
pass
|
||
|
elif derivative.is_Atom:
|
||
|
return derivative
|
||
|
else:
|
||
|
return derivative.fromiter(
|
||
|
[_as_finite_diff(ar, points, x0, wrt) for ar
|
||
|
in derivative.args], **derivative.assumptions0)
|
||
|
|
||
|
if wrt is None:
|
||
|
old = None
|
||
|
for v in derivative.variables:
|
||
|
if old is v:
|
||
|
continue
|
||
|
derivative = _as_finite_diff(derivative, points, x0, v)
|
||
|
old = v
|
||
|
return derivative
|
||
|
|
||
|
order = derivative.variables.count(wrt)
|
||
|
|
||
|
if x0 is None:
|
||
|
x0 = wrt
|
||
|
|
||
|
if not iterable(points):
|
||
|
if getattr(points, 'is_Function', False) and wrt in points.args:
|
||
|
points = points.subs(wrt, x0)
|
||
|
# points is simply the step-size, let's make it a
|
||
|
# equidistant sequence centered around x0
|
||
|
if order % 2 == 0:
|
||
|
# even order => odd number of points, grid point included
|
||
|
points = [x0 + points*i for i
|
||
|
in range(-order//2, order//2 + 1)]
|
||
|
else:
|
||
|
# odd order => even number of points, half-way wrt grid point
|
||
|
points = [x0 + points*S(i)/2 for i
|
||
|
in range(-order, order + 1, 2)]
|
||
|
others = [wrt, 0]
|
||
|
for v in set(derivative.variables):
|
||
|
if v == wrt:
|
||
|
continue
|
||
|
others += [v, derivative.variables.count(v)]
|
||
|
if len(points) < order+1:
|
||
|
raise ValueError("Too few points for order %d" % order)
|
||
|
return apply_finite_diff(order, points, [
|
||
|
Derivative(derivative.expr.subs({wrt: x}), *others) for
|
||
|
x in points], x0)
|
||
|
|
||
|
|
||
|
def differentiate_finite(expr, *symbols,
|
||
|
points=1, x0=None, wrt=None, evaluate=False):
|
||
|
r""" Differentiate expr and replace Derivatives with finite differences.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
expr : expression
|
||
|
\*symbols : differentiate with respect to symbols
|
||
|
points: sequence, coefficient or undefined function, optional
|
||
|
see ``Derivative.as_finite_difference``
|
||
|
x0: number or Symbol, optional
|
||
|
see ``Derivative.as_finite_difference``
|
||
|
wrt: Symbol, optional
|
||
|
see ``Derivative.as_finite_difference``
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import sin, Function, differentiate_finite
|
||
|
>>> from sympy.abc import x, y, h
|
||
|
>>> f, g = Function('f'), Function('g')
|
||
|
>>> differentiate_finite(f(x)*g(x), x, points=[x-h, x+h])
|
||
|
-f(-h + x)*g(-h + x)/(2*h) + f(h + x)*g(h + x)/(2*h)
|
||
|
|
||
|
``differentiate_finite`` works on any expression, including the expressions
|
||
|
with embedded derivatives:
|
||
|
|
||
|
>>> differentiate_finite(f(x) + sin(x), x, 2)
|
||
|
-2*f(x) + f(x - 1) + f(x + 1) - 2*sin(x) + sin(x - 1) + sin(x + 1)
|
||
|
>>> differentiate_finite(f(x, y), x, y)
|
||
|
f(x - 1/2, y - 1/2) - f(x - 1/2, y + 1/2) - f(x + 1/2, y - 1/2) + f(x + 1/2, y + 1/2)
|
||
|
>>> differentiate_finite(f(x)*g(x).diff(x), x)
|
||
|
(-g(x) + g(x + 1))*f(x + 1/2) - (g(x) - g(x - 1))*f(x - 1/2)
|
||
|
|
||
|
To make finite difference with non-constant discretization step use
|
||
|
undefined functions:
|
||
|
|
||
|
>>> dx = Function('dx')
|
||
|
>>> differentiate_finite(f(x)*g(x).diff(x), points=dx(x))
|
||
|
-(-g(x - dx(x)/2 - dx(x - dx(x)/2)/2)/dx(x - dx(x)/2) +
|
||
|
g(x - dx(x)/2 + dx(x - dx(x)/2)/2)/dx(x - dx(x)/2))*f(x - dx(x)/2)/dx(x) +
|
||
|
(-g(x + dx(x)/2 - dx(x + dx(x)/2)/2)/dx(x + dx(x)/2) +
|
||
|
g(x + dx(x)/2 + dx(x + dx(x)/2)/2)/dx(x + dx(x)/2))*f(x + dx(x)/2)/dx(x)
|
||
|
|
||
|
"""
|
||
|
if any(term.is_Derivative for term in list(preorder_traversal(expr))):
|
||
|
evaluate = False
|
||
|
|
||
|
Dexpr = expr.diff(*symbols, evaluate=evaluate)
|
||
|
if evaluate:
|
||
|
sympy_deprecation_warning("""
|
||
|
The evaluate flag to differentiate_finite() is deprecated.
|
||
|
|
||
|
evaluate=True expands the intermediate derivatives before computing
|
||
|
differences, but this usually not what you want, as it does not
|
||
|
satisfy the product rule.
|
||
|
""",
|
||
|
deprecated_since_version="1.5",
|
||
|
active_deprecations_target="deprecated-differentiate_finite-evaluate",
|
||
|
)
|
||
|
return Dexpr.replace(
|
||
|
lambda arg: arg.is_Derivative,
|
||
|
lambda arg: arg.as_finite_difference(points=points, x0=x0, wrt=wrt))
|
||
|
else:
|
||
|
DFexpr = Dexpr.as_finite_difference(points=points, x0=x0, wrt=wrt)
|
||
|
return DFexpr.replace(
|
||
|
lambda arg: isinstance(arg, Subs),
|
||
|
lambda arg: arg.expr.as_finite_difference(
|
||
|
points=points, x0=arg.point[0], wrt=arg.variables[0]))
|