123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807 |
- """Fourier Series"""
- from sympy.core.numbers import (oo, pi)
- from sympy.core.symbol import Wild
- from sympy.core.expr import Expr
- from sympy.core.add import Add
- from sympy.core.containers import Tuple
- from sympy.core.singleton import S
- from sympy.core.symbol import Dummy, Symbol
- from sympy.core.sympify import sympify
- from sympy.functions.elementary.trigonometric import sin, cos, sinc
- from sympy.series.series_class import SeriesBase
- from sympy.series.sequences import SeqFormula
- from sympy.sets.sets import Interval
- from sympy.simplify.fu import TR2, TR1, TR10, sincos_to_sum
- from sympy.utilities.iterables import is_sequence
- def fourier_cos_seq(func, limits, n):
- """Returns the cos sequence in a Fourier series"""
- from sympy.integrals import integrate
- x, L = limits[0], limits[2] - limits[1]
- cos_term = cos(2*n*pi*x / L)
- formula = 2 * cos_term * integrate(func * cos_term, limits) / L
- a0 = formula.subs(n, S.Zero) / 2
- return a0, SeqFormula(2 * cos_term * integrate(func * cos_term, limits)
- / L, (n, 1, oo))
- def fourier_sin_seq(func, limits, n):
- """Returns the sin sequence in a Fourier series"""
- from sympy.integrals import integrate
- x, L = limits[0], limits[2] - limits[1]
- sin_term = sin(2*n*pi*x / L)
- return SeqFormula(2 * sin_term * integrate(func * sin_term, limits)
- / L, (n, 1, oo))
- def _process_limits(func, limits):
- """
- Limits should be of the form (x, start, stop).
- x should be a symbol. Both start and stop should be bounded.
- Explanation
- ===========
- * If x is not given, x is determined from func.
- * If limits is None. Limit of the form (x, -pi, pi) is returned.
- Examples
- ========
- >>> from sympy.series.fourier import _process_limits as pari
- >>> from sympy.abc import x
- >>> pari(x**2, (x, -2, 2))
- (x, -2, 2)
- >>> pari(x**2, (-2, 2))
- (x, -2, 2)
- >>> pari(x**2, None)
- (x, -pi, pi)
- """
- def _find_x(func):
- free = func.free_symbols
- if len(free) == 1:
- return free.pop()
- elif not free:
- return Dummy('k')
- else:
- raise ValueError(
- " specify dummy variables for %s. If the function contains"
- " more than one free symbol, a dummy variable should be"
- " supplied explicitly e.g. FourierSeries(m*n**2, (n, -pi, pi))"
- % func)
- x, start, stop = None, None, None
- if limits is None:
- x, start, stop = _find_x(func), -pi, pi
- if is_sequence(limits, Tuple):
- if len(limits) == 3:
- x, start, stop = limits
- elif len(limits) == 2:
- x = _find_x(func)
- start, stop = limits
- if not isinstance(x, Symbol) or start is None or stop is None:
- raise ValueError('Invalid limits given: %s' % str(limits))
- unbounded = [S.NegativeInfinity, S.Infinity]
- if start in unbounded or stop in unbounded:
- raise ValueError("Both the start and end value should be bounded")
- return sympify((x, start, stop))
- def finite_check(f, x, L):
- def check_fx(exprs, x):
- return x not in exprs.free_symbols
- def check_sincos(_expr, x, L):
- if isinstance(_expr, (sin, cos)):
- sincos_args = _expr.args[0]
- if sincos_args.match(a*(pi/L)*x + b) is not None:
- return True
- else:
- return False
- _expr = sincos_to_sum(TR2(TR1(f)))
- add_coeff = _expr.as_coeff_add()
- a = Wild('a', properties=[lambda k: k.is_Integer, lambda k: k != S.Zero, ])
- b = Wild('b', properties=[lambda k: x not in k.free_symbols, ])
- for s in add_coeff[1]:
- mul_coeffs = s.as_coeff_mul()[1]
- for t in mul_coeffs:
- if not (check_fx(t, x) or check_sincos(t, x, L)):
- return False, f
- return True, _expr
- class FourierSeries(SeriesBase):
- r"""Represents Fourier sine/cosine series.
- Explanation
- ===========
- This class only represents a fourier series.
- No computation is performed.
- For how to compute Fourier series, see the :func:`fourier_series`
- docstring.
- See Also
- ========
- sympy.series.fourier.fourier_series
- """
- def __new__(cls, *args):
- args = map(sympify, args)
- return Expr.__new__(cls, *args)
- @property
- def function(self):
- return self.args[0]
- @property
- def x(self):
- return self.args[1][0]
- @property
- def period(self):
- return (self.args[1][1], self.args[1][2])
- @property
- def a0(self):
- return self.args[2][0]
- @property
- def an(self):
- return self.args[2][1]
- @property
- def bn(self):
- return self.args[2][2]
- @property
- def interval(self):
- return Interval(0, oo)
- @property
- def start(self):
- return self.interval.inf
- @property
- def stop(self):
- return self.interval.sup
- @property
- def length(self):
- return oo
- @property
- def L(self):
- return abs(self.period[1] - self.period[0]) / 2
- def _eval_subs(self, old, new):
- x = self.x
- if old.has(x):
- return self
- def truncate(self, n=3):
- """
- Return the first n nonzero terms of the series.
- If ``n`` is None return an iterator.
- Parameters
- ==========
- n : int or None
- Amount of non-zero terms in approximation or None.
- Returns
- =======
- Expr or iterator :
- Approximation of function expanded into Fourier series.
- Examples
- ========
- >>> from sympy import fourier_series, pi
- >>> from sympy.abc import x
- >>> s = fourier_series(x, (x, -pi, pi))
- >>> s.truncate(4)
- 2*sin(x) - sin(2*x) + 2*sin(3*x)/3 - sin(4*x)/2
- See Also
- ========
- sympy.series.fourier.FourierSeries.sigma_approximation
- """
- if n is None:
- return iter(self)
- terms = []
- for t in self:
- if len(terms) == n:
- break
- if t is not S.Zero:
- terms.append(t)
- return Add(*terms)
- def sigma_approximation(self, n=3):
- r"""
- Return :math:`\sigma`-approximation of Fourier series with respect
- to order n.
- Explanation
- ===========
- Sigma approximation adjusts a Fourier summation to eliminate the Gibbs
- phenomenon which would otherwise occur at discontinuities.
- A sigma-approximated summation for a Fourier series of a T-periodical
- function can be written as
- .. math::
- s(\theta) = \frac{1}{2} a_0 + \sum _{k=1}^{m-1}
- \operatorname{sinc} \Bigl( \frac{k}{m} \Bigr) \cdot
- \left[ a_k \cos \Bigl( \frac{2\pi k}{T} \theta \Bigr)
- + b_k \sin \Bigl( \frac{2\pi k}{T} \theta \Bigr) \right],
- where :math:`a_0, a_k, b_k, k=1,\ldots,{m-1}` are standard Fourier
- series coefficients and
- :math:`\operatorname{sinc} \Bigl( \frac{k}{m} \Bigr)` is a Lanczos
- :math:`\sigma` factor (expressed in terms of normalized
- :math:`\operatorname{sinc}` function).
- Parameters
- ==========
- n : int
- Highest order of the terms taken into account in approximation.
- Returns
- =======
- Expr :
- Sigma approximation of function expanded into Fourier series.
- Examples
- ========
- >>> from sympy import fourier_series, pi
- >>> from sympy.abc import x
- >>> s = fourier_series(x, (x, -pi, pi))
- >>> s.sigma_approximation(4)
- 2*sin(x)*sinc(pi/4) - 2*sin(2*x)/pi + 2*sin(3*x)*sinc(3*pi/4)/3
- See Also
- ========
- sympy.series.fourier.FourierSeries.truncate
- Notes
- =====
- The behaviour of
- :meth:`~sympy.series.fourier.FourierSeries.sigma_approximation`
- is different from :meth:`~sympy.series.fourier.FourierSeries.truncate`
- - it takes all nonzero terms of degree smaller than n, rather than
- first n nonzero ones.
- References
- ==========
- .. [1] https://en.wikipedia.org/wiki/Gibbs_phenomenon
- .. [2] https://en.wikipedia.org/wiki/Sigma_approximation
- """
- terms = [sinc(pi * i / n) * t for i, t in enumerate(self[:n])
- if t is not S.Zero]
- return Add(*terms)
- def shift(self, s):
- """
- Shift the function by a term independent of x.
- Explanation
- ===========
- f(x) -> f(x) + s
- This is fast, if Fourier series of f(x) is already
- computed.
- Examples
- ========
- >>> from sympy import fourier_series, pi
- >>> from sympy.abc import x
- >>> s = fourier_series(x**2, (x, -pi, pi))
- >>> s.shift(1).truncate()
- -4*cos(x) + cos(2*x) + 1 + pi**2/3
- """
- s, x = sympify(s), self.x
- if x in s.free_symbols:
- raise ValueError("'%s' should be independent of %s" % (s, x))
- a0 = self.a0 + s
- sfunc = self.function + s
- return self.func(sfunc, self.args[1], (a0, self.an, self.bn))
- def shiftx(self, s):
- """
- Shift x by a term independent of x.
- Explanation
- ===========
- f(x) -> f(x + s)
- This is fast, if Fourier series of f(x) is already
- computed.
- Examples
- ========
- >>> from sympy import fourier_series, pi
- >>> from sympy.abc import x
- >>> s = fourier_series(x**2, (x, -pi, pi))
- >>> s.shiftx(1).truncate()
- -4*cos(x + 1) + cos(2*x + 2) + pi**2/3
- """
- s, x = sympify(s), self.x
- if x in s.free_symbols:
- raise ValueError("'%s' should be independent of %s" % (s, x))
- an = self.an.subs(x, x + s)
- bn = self.bn.subs(x, x + s)
- sfunc = self.function.subs(x, x + s)
- return self.func(sfunc, self.args[1], (self.a0, an, bn))
- def scale(self, s):
- """
- Scale the function by a term independent of x.
- Explanation
- ===========
- f(x) -> s * f(x)
- This is fast, if Fourier series of f(x) is already
- computed.
- Examples
- ========
- >>> from sympy import fourier_series, pi
- >>> from sympy.abc import x
- >>> s = fourier_series(x**2, (x, -pi, pi))
- >>> s.scale(2).truncate()
- -8*cos(x) + 2*cos(2*x) + 2*pi**2/3
- """
- s, x = sympify(s), self.x
- if x in s.free_symbols:
- raise ValueError("'%s' should be independent of %s" % (s, x))
- an = self.an.coeff_mul(s)
- bn = self.bn.coeff_mul(s)
- a0 = self.a0 * s
- sfunc = self.args[0] * s
- return self.func(sfunc, self.args[1], (a0, an, bn))
- def scalex(self, s):
- """
- Scale x by a term independent of x.
- Explanation
- ===========
- f(x) -> f(s*x)
- This is fast, if Fourier series of f(x) is already
- computed.
- Examples
- ========
- >>> from sympy import fourier_series, pi
- >>> from sympy.abc import x
- >>> s = fourier_series(x**2, (x, -pi, pi))
- >>> s.scalex(2).truncate()
- -4*cos(2*x) + cos(4*x) + pi**2/3
- """
- s, x = sympify(s), self.x
- if x in s.free_symbols:
- raise ValueError("'%s' should be independent of %s" % (s, x))
- an = self.an.subs(x, x * s)
- bn = self.bn.subs(x, x * s)
- sfunc = self.function.subs(x, x * s)
- return self.func(sfunc, self.args[1], (self.a0, an, bn))
- def _eval_as_leading_term(self, x, logx=None, cdir=0):
- for t in self:
- if t is not S.Zero:
- return t
- def _eval_term(self, pt):
- if pt == 0:
- return self.a0
- return self.an.coeff(pt) + self.bn.coeff(pt)
- def __neg__(self):
- return self.scale(-1)
- def __add__(self, other):
- if isinstance(other, FourierSeries):
- if self.period != other.period:
- raise ValueError("Both the series should have same periods")
- x, y = self.x, other.x
- function = self.function + other.function.subs(y, x)
- if self.x not in function.free_symbols:
- return function
- an = self.an + other.an
- bn = self.bn + other.bn
- a0 = self.a0 + other.a0
- return self.func(function, self.args[1], (a0, an, bn))
- return Add(self, other)
- def __sub__(self, other):
- return self.__add__(-other)
- class FiniteFourierSeries(FourierSeries):
- r"""Represents Finite Fourier sine/cosine series.
- For how to compute Fourier series, see the :func:`fourier_series`
- docstring.
- Parameters
- ==========
- f : Expr
- Expression for finding fourier_series
- limits : ( x, start, stop)
- x is the independent variable for the expression f
- (start, stop) is the period of the fourier series
- exprs: (a0, an, bn) or Expr
- a0 is the constant term a0 of the fourier series
- an is a dictionary of coefficients of cos terms
- an[k] = coefficient of cos(pi*(k/L)*x)
- bn is a dictionary of coefficients of sin terms
- bn[k] = coefficient of sin(pi*(k/L)*x)
- or exprs can be an expression to be converted to fourier form
- Methods
- =======
- This class is an extension of FourierSeries class.
- Please refer to sympy.series.fourier.FourierSeries for
- further information.
- See Also
- ========
- sympy.series.fourier.FourierSeries
- sympy.series.fourier.fourier_series
- """
- def __new__(cls, f, limits, exprs):
- f = sympify(f)
- limits = sympify(limits)
- exprs = sympify(exprs)
- if not (isinstance(exprs, Tuple) and len(exprs) == 3): # exprs is not of form (a0, an, bn)
- # Converts the expression to fourier form
- c, e = exprs.as_coeff_add()
- rexpr = c + Add(*[TR10(i) for i in e])
- a0, exp_ls = rexpr.expand(trig=False, power_base=False, power_exp=False, log=False).as_coeff_add()
- x = limits[0]
- L = abs(limits[2] - limits[1]) / 2
- a = Wild('a', properties=[lambda k: k.is_Integer, lambda k: k is not S.Zero, ])
- b = Wild('b', properties=[lambda k: x not in k.free_symbols, ])
- an = dict()
- bn = dict()
- # separates the coefficients of sin and cos terms in dictionaries an, and bn
- for p in exp_ls:
- t = p.match(b * cos(a * (pi / L) * x))
- q = p.match(b * sin(a * (pi / L) * x))
- if t:
- an[t[a]] = t[b] + an.get(t[a], S.Zero)
- elif q:
- bn[q[a]] = q[b] + bn.get(q[a], S.Zero)
- else:
- a0 += p
- exprs = Tuple(a0, an, bn)
- return Expr.__new__(cls, f, limits, exprs)
- @property
- def interval(self):
- _length = 1 if self.a0 else 0
- _length += max(set(self.an.keys()).union(set(self.bn.keys()))) + 1
- return Interval(0, _length)
- @property
- def length(self):
- return self.stop - self.start
- def shiftx(self, s):
- s, x = sympify(s), self.x
- if x in s.free_symbols:
- raise ValueError("'%s' should be independent of %s" % (s, x))
- _expr = self.truncate().subs(x, x + s)
- sfunc = self.function.subs(x, x + s)
- return self.func(sfunc, self.args[1], _expr)
- def scale(self, s):
- s, x = sympify(s), self.x
- if x in s.free_symbols:
- raise ValueError("'%s' should be independent of %s" % (s, x))
- _expr = self.truncate() * s
- sfunc = self.function * s
- return self.func(sfunc, self.args[1], _expr)
- def scalex(self, s):
- s, x = sympify(s), self.x
- if x in s.free_symbols:
- raise ValueError("'%s' should be independent of %s" % (s, x))
- _expr = self.truncate().subs(x, x * s)
- sfunc = self.function.subs(x, x * s)
- return self.func(sfunc, self.args[1], _expr)
- def _eval_term(self, pt):
- if pt == 0:
- return self.a0
- _term = self.an.get(pt, S.Zero) * cos(pt * (pi / self.L) * self.x) \
- + self.bn.get(pt, S.Zero) * sin(pt * (pi / self.L) * self.x)
- return _term
- def __add__(self, other):
- if isinstance(other, FourierSeries):
- return other.__add__(fourier_series(self.function, self.args[1],\
- finite=False))
- elif isinstance(other, FiniteFourierSeries):
- if self.period != other.period:
- raise ValueError("Both the series should have same periods")
- x, y = self.x, other.x
- function = self.function + other.function.subs(y, x)
- if self.x not in function.free_symbols:
- return function
- return fourier_series(function, limits=self.args[1])
- def fourier_series(f, limits=None, finite=True):
- r"""Computes the Fourier trigonometric series expansion.
- Explanation
- ===========
- Fourier trigonometric series of $f(x)$ over the interval $(a, b)$
- is defined as:
- .. math::
- \frac{a_0}{2} + \sum_{n=1}^{\infty}
- (a_n \cos(\frac{2n \pi x}{L}) + b_n \sin(\frac{2n \pi x}{L}))
- where the coefficients are:
- .. math::
- L = b - a
- .. math::
- a_0 = \frac{2}{L} \int_{a}^{b}{f(x) dx}
- .. math::
- a_n = \frac{2}{L} \int_{a}^{b}{f(x) \cos(\frac{2n \pi x}{L}) dx}
- .. math::
- b_n = \frac{2}{L} \int_{a}^{b}{f(x) \sin(\frac{2n \pi x}{L}) dx}
- The condition whether the function $f(x)$ given should be periodic
- or not is more than necessary, because it is sufficient to consider
- the series to be converging to $f(x)$ only in the given interval,
- not throughout the whole real line.
- This also brings a lot of ease for the computation because
- you don't have to make $f(x)$ artificially periodic by
- wrapping it with piecewise, modulo operations,
- but you can shape the function to look like the desired periodic
- function only in the interval $(a, b)$, and the computed series will
- automatically become the series of the periodic version of $f(x)$.
- This property is illustrated in the examples section below.
- Parameters
- ==========
- limits : (sym, start, end), optional
- *sym* denotes the symbol the series is computed with respect to.
- *start* and *end* denotes the start and the end of the interval
- where the fourier series converges to the given function.
- Default range is specified as $-\pi$ and $\pi$.
- Returns
- =======
- FourierSeries
- A symbolic object representing the Fourier trigonometric series.
- Examples
- ========
- Computing the Fourier series of $f(x) = x^2$:
- >>> from sympy import fourier_series, pi
- >>> from sympy.abc import x
- >>> f = x**2
- >>> s = fourier_series(f, (x, -pi, pi))
- >>> s1 = s.truncate(n=3)
- >>> s1
- -4*cos(x) + cos(2*x) + pi**2/3
- Shifting of the Fourier series:
- >>> s.shift(1).truncate()
- -4*cos(x) + cos(2*x) + 1 + pi**2/3
- >>> s.shiftx(1).truncate()
- -4*cos(x + 1) + cos(2*x + 2) + pi**2/3
- Scaling of the Fourier series:
- >>> s.scale(2).truncate()
- -8*cos(x) + 2*cos(2*x) + 2*pi**2/3
- >>> s.scalex(2).truncate()
- -4*cos(2*x) + cos(4*x) + pi**2/3
- Computing the Fourier series of $f(x) = x$:
- This illustrates how truncating to the higher order gives better
- convergence.
- .. plot::
- :context: reset
- :format: doctest
- :include-source: True
- >>> from sympy import fourier_series, pi, plot
- >>> from sympy.abc import x
- >>> f = x
- >>> s = fourier_series(f, (x, -pi, pi))
- >>> s1 = s.truncate(n = 3)
- >>> s2 = s.truncate(n = 5)
- >>> s3 = s.truncate(n = 7)
- >>> p = plot(f, s1, s2, s3, (x, -pi, pi), show=False, legend=True)
- >>> p[0].line_color = (0, 0, 0)
- >>> p[0].label = 'x'
- >>> p[1].line_color = (0.7, 0.7, 0.7)
- >>> p[1].label = 'n=3'
- >>> p[2].line_color = (0.5, 0.5, 0.5)
- >>> p[2].label = 'n=5'
- >>> p[3].line_color = (0.3, 0.3, 0.3)
- >>> p[3].label = 'n=7'
- >>> p.show()
- This illustrates how the series converges to different sawtooth
- waves if the different ranges are specified.
- .. plot::
- :context: close-figs
- :format: doctest
- :include-source: True
- >>> s1 = fourier_series(x, (x, -1, 1)).truncate(10)
- >>> s2 = fourier_series(x, (x, -pi, pi)).truncate(10)
- >>> s3 = fourier_series(x, (x, 0, 1)).truncate(10)
- >>> p = plot(x, s1, s2, s3, (x, -5, 5), show=False, legend=True)
- >>> p[0].line_color = (0, 0, 0)
- >>> p[0].label = 'x'
- >>> p[1].line_color = (0.7, 0.7, 0.7)
- >>> p[1].label = '[-1, 1]'
- >>> p[2].line_color = (0.5, 0.5, 0.5)
- >>> p[2].label = '[-pi, pi]'
- >>> p[3].line_color = (0.3, 0.3, 0.3)
- >>> p[3].label = '[0, 1]'
- >>> p.show()
- Notes
- =====
- Computing Fourier series can be slow
- due to the integration required in computing
- an, bn.
- It is faster to compute Fourier series of a function
- by using shifting and scaling on an already
- computed Fourier series rather than computing
- again.
- e.g. If the Fourier series of ``x**2`` is known
- the Fourier series of ``x**2 - 1`` can be found by shifting by ``-1``.
- See Also
- ========
- sympy.series.fourier.FourierSeries
- References
- ==========
- .. [1] https://mathworld.wolfram.com/FourierSeries.html
- """
- f = sympify(f)
- limits = _process_limits(f, limits)
- x = limits[0]
- if x not in f.free_symbols:
- return f
- if finite:
- L = abs(limits[2] - limits[1]) / 2
- is_finite, res_f = finite_check(f, x, L)
- if is_finite:
- return FiniteFourierSeries(f, limits, res_f)
- n = Dummy('n')
- center = (limits[1] + limits[2]) / 2
- if center.is_zero:
- neg_f = f.subs(x, -x)
- if f == neg_f:
- a0, an = fourier_cos_seq(f, limits, n)
- bn = SeqFormula(0, (1, oo))
- return FourierSeries(f, limits, (a0, an, bn))
- elif f == -neg_f:
- a0 = S.Zero
- an = SeqFormula(0, (1, oo))
- bn = fourier_sin_seq(f, limits, n)
- return FourierSeries(f, limits, (a0, an, bn))
- a0, an = fourier_cos_seq(f, limits, n)
- bn = fourier_sin_seq(f, limits, n)
- return FourierSeries(f, limits, (a0, an, bn))
|