.. _discrete_time: ===================== Discrete-time signals ===================== There are a number of domain variables for discrete-time signals: - `n` for discrete-time signals, for example, `3 * u(n - 2)` - `f` for linear frequency from a DTFT - `F` for normalized linear frequency, `F = f * dt` - `k` for discrete-frequency spectra - `omega` for angular frequency from a DTFT, `omega = 2 * pi * f` - `z` for z-transforms, for example, `Y(z)` - `Omega` for normalized angular frequency from a DTFT, `Omega = omega * dt` The `n`, `k`, and `z` variables share many of the attributes and methods of their continuous-time equivalents, `t`, `f`, and `s`, :ref:`expressions`. The discrete-time signal can be plotted using the `plot()` method. For example: .. literalinclude:: examples/discretetime/dt1-plot1.py .. image:: examples/discretetime/dt1-plot1.png :width: 15cm A complex discrete-time signal can be plotted in polar coordinates using the `plot()` method with the `polar` argument, for example: .. literalinclude:: examples/discretetime/cdt1-plot1.py .. image:: examples/discretetime/cdt1-plot1.png :width: 10cm Functions ========= There are two special discrete time functions: - `delta(n)` or `ui(n)` or `UnitImpulse(n)`: the discrete unit impulse. This is one when `n=0` and zero otherwise. - `u(n)` or `us(n)` or `UnitStep(n)`: the discrete unit step. This is one when `n>=0` and zero otherwise. Sequences ========= Generic sequences can be created using the `seq` function. For example:: >>> s = seq((1, 2, 3)) {_1, 2, 3} Note, the underscore marks the element in the sequence where `n = 0`. By default, `seq()` creates a discrete-time domain sequence. The domain can be specified with the `domain` argument. This can be either `n`, `k`, or `z`. For example, a Z-domain sequence is created with:: >>> s = seq((1, 2, 3), domain=z) {_1, 2, 3} Here's an example where the sequence is specified as a string:: >>> s = seq('1, _2, 3') {1, _2, 3} Sequences can also be generated from a discrete-time expression, for example:: >>> x = delta(n) + 2 * delta(n - 2) >>> seq = x.seq((-5, 5)) >>> seq {0, 0, 0, 0, 0, _1, 0, 2, 0, 0, 0} Note, the underscore marks the origin; the element in the sequence where `n = 0`. Sequences can have quantities, for example, a discrete-time voltage sequence is created with:: >>> v = voltage(seq((1, 2, 3), domain=n)) The extent of a sequence is given by the `extent` attribute. >>> seq.extent >>> 3 Each element in a sequence has a sequence index. The sequence indices are return as a list by the `n` attribute. For example:: >>> x = seq('1, _2, 3, 4') >>> x.n [-1, 0, 1, 2] The origin of a sequence is given by the `origin` attribute. This indicates the element index where `n = 0`. For example:: >>> x = seq('1, _2, 3, 4') >>> x.origin 1 >>> x = seq('1, 2, _3, 4') >>> x.origin 2 The origin can be changed:: >>> x = seq('1, 2, _3, 4') >>> x.origin = 1 >>> x {1, _2, 3, 4} Specific elements in the sequence can be accessed using call notation: >>> x = seq('1, _2, 3, 4') >>> x(0) 2 >>> x(1) 3 Specific elements can also be accessed using array notation. Note, the argument specifies the element sequence index, for example:: >>> x = seq('1, _2, 3, 4') >>> x[0] 2 >>> x[1] 3 If you want the first element convert the sequence to a list or ndarray, for example:: >>> x = seq('1, _2, 3, 4') >>> array(x)[0] 1 Sequences behave like lists and thus the `+` operator concatenates sequences:: >>> seq((1, 2, 3)) + seq('{4, 5}') {_1, 2, 3, 4, 5} Note, this ignores the origins. Similarly, the `*` operator repeats sequences a specified number of times, for example:: >>> seq((1, 2, 3)) * 2 {_1, 2, 3, 1, 2, 3} To add sequences element by element, it is necessary to explicitly convert each sequence to an array, add the arrays assuming they are equal length, and convert back to a sequence, for example:: >>> seq(seq((1, 2, 3)).as_array() + seq('{4, _5, 6}').as_array()) {_5, 7, 9} Note, this ignores the origins. Sequences can be convolved, for example:: >>> seq((1, 2, 3)).convolve(seq((1, 1)) {_1, 3, 5, 3} Sequences can be evaluated and converted to a new sequence of floating point values using the `evalf()` method. This has an argument to specify the number of decimal places. For example:: >>> seq((pi, pi * 2)) {_π, 2⋅π} >>> seq((pi, pi * 2)).evalf(3) {_3.14, 6.28} Sequences can be evaluated and converted to a NumPy array with the `as_array()` method:: >>> x = seq('1, _2, 3, 4') >>> a = x.as_array() >>> a array([1., 2., 3., 4.]) Alternatively, the `evaluate()` method can be used to access and convert a single element or multiple elements. If the argument is a scalar, a real or complex Python scalar is returned. If the argument is iterable (tuple, list, ndarray), a NumPy real or complex ndarray is returned. Note, argument values outside the sequence return zero. Here is an example:: >>> x = seq('1, _2, 3, 4') >>> x.evaluate(5) 0 >>> a = x.evaluate((1, 2)) >>> a array([3., 4.]) Sequences can be converted to discrete-time domain or discrete-frequency domain expressions, for example:: >>> seq((1, 2)).expr δ[n] + 2⋅δ[n - 2] The discrete Fourier transform (DFT), inverse discrete Fourier transform (IDFT) z-transform (ZT), and inverse z-transform (IZT) can be performed using the `DFT()`, `IDFT()`, `ZT()`, and `IZT()` methods. In each case, a new sequence is returned. For example:: >>> seq((1, 2, 3, 4)).ZT() ⎧ 2 3 4 ⎫ ⎪_1, ─, ──, ──⎪ ⎨ z 2 3⎬ ⎪ z z ⎪ ⎩ ⎭ >>> seq((1, 2, 3, 4)).DFT() {_10, -2 + 2⋅ⅉ, -2, -2 - 2⋅ⅉ} Sequence operators ------------------ Lcapy overloads the leftshift operator and the rightshift operator to shift sequences. For example:: >>> a = seq((1, 2, 3)) >>> a >> 2 {_0, 0, 1, 2, 3} >>> a << 2 {1, 2, _3} Sequence attributes ------------------- - `expr` convert to a discrete-time or discrete-frequency expression - `extent` the extent of the sequence - `n` the sequence indices - `origin` the element index for `n = 0` - `vals` the sequence values as a list Sequence methods ---------------- These methods do not modify the sequence but return a new sequence, NumPy ndarray, or, Lcapy expression. - `as_array()` convert to NumPy ndarray - `as_impulses()` convert to a weighted sum of unit impulses expression - `convolve()` convolve with another sequence - `delay()` delay by an integer number of samples (the sequence is advanced if the argument is negative) - `DFT()` compute discrete Fourier transform as a sequence - `DTFT()` compute discrete-time Fourier transform - `evalf()` convert each element in sequence to a SymPy floating-point value with a specified number of digits - `evaluate()` evaluate sequence at specified indices and return as NumPy ndarray - `IDFT()` compute inverse discrete Fourier transform as a sequence - `IZT()` compute inverse z-transform as a sequence - `lfilter()` filter by DLTI filter - `simplify()` simplify each expression in sequence - `prune()` remove zeroes from the ends of the sequence - `plot()` plot sequence as a lollipop (stem) plot - `zeroextend()` add zeroes at either start or end so origin is included - `zeropad()` add zeroes to the end of the sequence - `ZT()` compute z-transform as a sequence Discrete-time (n-domain) expressions ==================================== Lcapy refers to Discrete-time expressions as n-domain expressions. They are of class `DiscreteTimeDomainExpression` and can be created explicitly using the n-domain variable `n`. For example:: >>> 2 * u(n) + delta(n - 1) 2⋅u[n] + δ[n - 1] In this expression `u(n)` denotes the unit step and `delta(n)` denotes the unit impulse. Square brackets are used in printing to reduce confusion with the Heaviside function and Dirac delta. Discrete-time expressions can be converted to sequences using the `seq()` method. For example:: >>> (delta(n) + 2 * delta(n - 1) + 3 * delta(n - 3)).seq() {_1, 2, 0, 3} The `seq()` method has an argument to specify the extent of the sequence. This is required if the sequences have infinite extent. For example:: >>> (2 * u(n) + delta(n - 1)).seq((-10, 10)) {_2, 3, 2, 2, 2, 2, 2, 2, 2, 2} In this example the zero samples have been removed but the sequence has been truncated. The z-transform of a discrete-time expression can be found with the `ZT()` method: >>> (delta(n) + 2 * delta(n - 2)).ZT() 2 1 + ── 2 z A more compact notation is to pass `z` as an argument: >>> (delta(n) + 2 * delta(n - 2))(z) 2 1 + ── 2 z The discrete-time Fourier transform (DTFT) of a discrete-time expression can be found with the `DTFT()` method: >>> (delta(n) + 2 * delta(n - 2)).DTFT() -4⋅ⅉ⋅π⋅Δₜ⋅f 1 + 2⋅ℯ A more compact notation is to pass `f` as an argument: >>> (delta(n) + 2 * delta(n - 2))(f) -4⋅ⅉ⋅π⋅Δₜ⋅f 1 + 2⋅ℯ The discrete Fourier transform (DFT) converts a discrete-time expression to a discrete-frequency expression. This is performed using the `DFT()` method or using a `k` argument. For example:: >>> (delta(n) + 2 * delta(n - 2))(k) -4⋅ⅉ⋅π⋅k ───────── N 1 + 2⋅ℯ If `N` is known, it can be specified as an argument. For example:: >>> (delta(n) + 2 * delta(n - 2))(k, N=4) -ⅉ⋅π⋅k 1 + 2⋅ℯ Evaluation of the DFT can be prevented by setting `evaluate=False`, >>> (delta(n) + 2 * delta(n - 2))(k, N=4, evaluate=False) N ____ ╲ ╲ -2⋅ⅉ⋅π⋅k⋅n ╲ ─────────── ╱ N ╱ (δ[n] + 2⋅δ[n - 2])⋅ℯ ╱ ‾‾‾‾ n = 0 Discrete-frequency (k-domain) expressions ========================================= Lcapy refers to discrete-frequency expressions as k-domain expressions. They are of class `DiscreteFourierDomainExpression` and can be created explicitly using the k-domain variable `k`. For example:: >>> 2 * u(k) + delta(k - 1) 2⋅u[k] + δ[k - 1] Discrete-frequency expressions can be converted to sequences using the `seq()` method. For example:: >>> (delta(k) + 2 * delta(k - 1) + 3 * delta(k - 3)).seq() {_1, 2, 0, 3} Z-domain expressions ==================== Z-domain expressions can be constructed using the z-domain variable `z`, for example:: >>> 1 + 1 / z 1 1 + ─ z Alternatively, they can be generated using a z-transform of a discrete-time signal. Z-domain expressions are objects of the `ZDomainExpression` class. They are functions of the complex variable `z` and are similar to `LaplaceDomainExpression` objects. The general form of a z-domain expression is a rational function so all the s-domain formatting methods are applicable (see :ref:`expressionsprinting`). The poles and zeros of a z-domain expression can be plotted using the `plot()` method. For example: .. literalinclude:: examples/discretetime/dt1-pole-zero-plot1.py .. image:: examples/discretetime/dt1-pole-zero-plot1.png :width: 15cm Transforms ========== Lcapy implements a number of transforms for converting between different domains. The explicit methods are: - `DFT()` Discrete Fourier transform - `DTFT()` Discrete-time Fourier transform - `ZT()` Z-transform - `IDFT()` Inverse discrete Fourier transform - `IDTFT()` Inverse discrete-time Fourier transform - `IZT()` Inverse z-transform Z-transform (ZT) ---------------- Lcapy uses the unilateral z-transform, defined as: .. math:: X(z) = \sum_{n=0}^{\infty} x(n) z^{-n} The z-transform is performed explicitly with the `ZT()` method: >>> x = delta(n) + 2 * delta(n - 2) >>> x.ZT() >>> 2 1 + ── 2 z It is also performed implicitly with `z` as an argument: >>> x(z) >>> 2 1 + ── 2 z Inverse z-transform (IZT) ------------------------- The inverse unilateral z-transform is not unique and is only defined for :math:`n \ge 0`. For example:: >>> H = z / (z - 'a') >>> H(n) ⎧ n ⎨a for n ≥ 0 ⎩ If the result is known to be causal, then use: >>> H(n, causal=True) n a ⋅u(n) Discrete time Fourier transform (DTFT) -------------------------------------- The DTFT converts an n-domain or z-domain expression into the f-domain (continuous Fourier domain). Note, unlike the Fourier transform, this is periodic with period :math:`1/\Delta t`. It is defined by .. math:: X_{\frac{1}{\Delta t}}(f) = \sum_{n=-\infty}^{\infty} x(n) e^{-2 \mathrm{j} \pi n \Delta t f} If :math:`x(n)` is the impulse response of a causal and stable DLTI system, the DTFT can be found by substituting :math:`z = \exp(-2 \mathrm{j} \pi \Delta t f)` into the z-transform of :math:`x(n)`. Here is an example:: >>> sign(n).DTFT() 2 ──────────────── -2⋅ⅉ⋅π⋅Δₜ⋅f 1 - ℯ Alternatively, the transform can be invoked using `f` as an argument:: >>> sign(n)(f) 2 ──────────────── -2⋅ⅉ⋅π⋅Δₜ⋅f 1 - ℯ Here's an example of plotting the DTFT: .. literalinclude:: examples/discretetime/dt1-DTFT-plot1.py .. image:: examples/discretetime/dt1-DTFT-plot1.png :width: 15cm The DTFT can be confusing due to the number of definitions commonly used. Due to the periodicity it is common to define a normalized frequency :math:`F = f \Delta t` and so .. math:: X_1(F) = \sum_{n=-\infty}^{\infty} x(n) e^{-2 \mathrm{j} \pi n F} Here is an example:: >>> sign(n).DTFT(F) 2 ───────────── -2⋅ⅉ⋅π⋅F 1 - ℯ Alternatively, the transform can be invoked using `F` as an argument:: >>> sign(n)(F) 2 ───────────── -2⋅ⅉ⋅π⋅F 1 - ℯ Another option is to use normalized angular frequency :math:`\Omega = 2\pi f \Delta t` .. math:: X_{2\pi}(\Omega) = \sum_{n=-\infty}^{\infty} x(n) e^{-\mathrm{j} n \Omega} Here is an example:: >>> sign(n).DTFT(Omega) 2 ──────────────── -2⋅ⅉ⋅π⋅Δₜ⋅f 1 - ℯ Alternatively, the transform can be invoked using `Omega` as an argument:: >>> sign(n)(Omega) 2 ──────────────── -2⋅ⅉ⋅π⋅Δₜ⋅f 1 - ℯ A normalized discrete-time angular Fourier transform of `x(n)` can be plotted as follows: >>> x.DTFT(Omega).plot() This plots the normalized angular frequency between :math:`-\pi` and :math:`\pi`. The DTFT, :math:`X_{\frac{1}{\Delta t}}(f)`, is related to the Fourier transform, :math:`X(f)`, by .. math:: X_{\frac{1}{\Delta t}}(f) = \frac{1}{\Delta t} \sum_{m=-\infty}^{\infty} X\left(f-\frac{m}{\Delta t}\right) Note, some definitions do not include the scale factor :math:`1 / \Delta t` since it assumed that :math:`x(n) = \Delta t x(n \Delta t)`. However, this introduces units confusion. The DTFT is periodic in frequency with a period :math:`1 / \Delta t` and provided the signal is not aliased, all the information about the signal can be obtained from any frequency range of interval :math:`1 / \Delta t`. By default Lcapy returns an expression showing the infinite number of spectral images. For example, >>> nexpr(1).DTFT() ∞ ____ ╲ ╲ ╲ ⎛ m ⎞ ╱ δ⎜f - ──⎟ ╱ ⎝ Δₜ⎠ ╱ ‾‾‾‾ m = -∞ ──────────────── Δₜ All the images can be removed with the `remove_images()` method. For example:: >>> nexpr(1).DTFT().remove_images() δ(f) ──── Δₜ Alternatively, the `images` argument can be used with the `DTFT()` method:: >>> nexpr(1).DTFT(images=0) δ(f) ──── Δₜ The number of images can be specified with the `m1` and `m2` arguments to the `remove_images()` method. This is useful for plotting. For example, >>> nexpr(1).DTFT(F).remove_images(-2, 2).doit() δ(F) + δ(F - 2) + δ(F - 1) + δ(F + 1) + δ(F + 2) Inverse discrete-time Fourier transform (IDTFT) ----------------------------------------------- Like the DTFT, the IDFT has many commonly used definitions. In terms of linear frequency, .. math:: x(n) = \Delta t \int_{-\frac{1}{2\Delta t}}^{\frac{1}{2\Delta t}} X_{\frac{1}{\Delta t}}(f) e^{2 \mathrm{j} \pi n \Delta t f} \mathrm{d}f where :math:`x(n)` denotes :math:`x(n \Delta t)`. In terms of normalized linear frequency, .. math:: x(n) = \int_{-\frac{1}{2}}^{\frac{1}{2}} X_{1}(f) e^{2 \mathrm{j} \pi n F} \mathrm{d}F In terms of normalized angular frequency, .. math:: x(n) = \frac{1}{2\pi} \int_{-\pi}^{\pi} X_{2\pi}(f) e^{\mathrm{j} n \Omega} \mathrm{d}\Omega Discrete Fourier transform (DFT) -------------------------------- The DFT converts an n-domain expression to a k-domain expression. The definition used by Lcapy is: .. math:: X(k) = \sum_{k=0}^{N - 1} x(n) e^{\frac{-\mathrm{j} 2\pi k n}{N}} Note, both :math:`x(n)` and :math:`X(k)` are assumed to periodic with period :math:`N`, i.e., :math:`x(n + m N) = x(n)` for integer :math:`m`. Inverse discrete Fourier transform (IDFT) ----------------------------------------- The IDFT converts a k-domain expression to an n-domain expression. The definition used by Lcapy is: .. math:: x(n) = \frac{1}{N} \sum_{k=0}^{N - 1} X(k) e^{\frac{\mathrm{j} 2 \pi k n}{N}} Again, both :math:`x(n)` and :math:`X(k)` are assumed to periodic with period :math:`N`, i.e., :math:`x(n + m N) = x(n)` for integer :math:`m`. Bilinear transform ------------------ The bilinear transform can be used to approximate an s-domain expression with a z-domain expression using :math:`s \approx \frac{2}{\Delta t} \frac{1 - z^{-1}}{1 + z^{-1}}` (see :ref:`discrete-time-approximation` for other methods). This is performed by the `bilinear_transform()` method of s-domain objects, for example:: >>> H = s / (s - 'a') >>> Hz = H.bilinear_transform().simplify() >>> Hz 2⋅(1 - z) ────────────────────── Δₜ⋅a⋅(z + 1) - 2⋅z + 2 The related method `inverse_bilinear_transform()` converts an s-domain expression to the z-domain using :math:`z \approx (1 + 0.5 \Delta t s) / (1 - 0.5 \Delta t s)`. Here's an example of the bilinear transform applied for an RC low-pass filter. >>> from lcapy import Circuit, s, t >>> net = Circuit(""" R 1 2; right W 0 0_2; right C 2 0_2; down W 2 3; right=0.5 W 0_2 0_3; right=0.5""") This has a transfer function: >>> H = net.transfer(1, 0, 3, 0) >>> H 1 ───────────── ⎛ 1 ⎞ C⋅R⋅⎜s + ───⎟ ⎝ C⋅R⎠ and an impulse response: >>> H(t) -t ─── C⋅R e ⋅u(t) ───────── C⋅R Using the bilinear transform, the discrete-time transfer function is >>> H.bilinear_transform().canonical() Δₜ⋅(z + 1) ────────────────────────────── ⎛ -2⋅C⋅R + Δₜ⎞ ⎜z + ───────────⎟⋅(2⋅C⋅R + Δₜ) ⎝ 2⋅C⋅R + Δₜ⎠ with a discrete-time impulse response >>> from lcapy.discretetime import n >>> H.bilinear_transform()(n).simplify() ⎛ n ⎞ ⎜ ⎛2⋅C⋅R - Δₜ⎞ ⎟ Δₜ⋅⎜4⋅C⋅R⋅⎜──────────⎟ ⋅u(n) - (2⋅C⋅R + Δₜ)⋅δ[n]⎟ ⎝ ⎝2⋅C⋅R + Δₜ⎠ ⎠ ───────────────────────────────────────────────── (2⋅C⋅R - Δₜ)⋅(2⋅C⋅R + Δₜ) Discrete-time systems ===================== See :ref:`discrete_time_systems`.