.. _systems:


=======================
Continuous-time systems
=======================


Linear time invariant filters
=============================

Linear time invariant filters are created with the `LTIFilter` class::

    >>> fil = LTIFilter(b, a)

where `b` is a list or array of the transfer function numerator
 coefficients and `a` is a list or array of the transfer function
 denominator coefficients.  For example,

    >>> fil = LTIFilter(('b0', ), ('a0', 'a1'))

The coefficients can be found from the `a` and `b` attributes::

   >>> fil.a
   (a₀, a₁)

   >>> fil.b
   (b₀,)

The filter's transfer function is found with the `transfer_function()` method::

   >>> fil.transfer_function()

       b₀
   ─────────
   a₀⋅s + a₁

its frequency response is found with the `frequency_response()` method::

   >>> fil.frequency_response()
          b₀
   ───────────────
   2⋅ⅉ⋅π⋅a₀⋅f + a₁

its angular frequency response is found with the `angular_frequency_response()` method::

   >>> fil.angular_frequency_response()
         b₀
     ──────────
     ⅉ⋅a₀⋅ω + a₁

its group delay is found with the `group_delay()` method::

   >>> fil.group_delay()
           a₀⋅a₁
    ─────────────────
       2   2  2     2
    4⋅π ⋅a₀ ⋅f  + a₁

its impulse response is found with the `impulse_response()` method::

   >>> fil.impulse_response()
       -a₁⋅t
       ──────
         a₀
   b₀⋅ℯ      ⋅u(t)
   ───────────────
          a₀

and its step response is found with the `step_response()` method::

   >>> fil.step_response()
      ⎛      -a₁⋅t ⎞
      ⎜      ──────⎟
      ⎜        a₀  ⎟
      ⎜1    ℯ      ⎟
   b₀⋅⎜── - ───────⎟⋅u(t)
      ⎝a₁      a₁  ⎠

The filter's differential equation is found with the `differential_equation()` method::

   >>> fil.differential_equation()
                  d
   a₀⋅y(t) = - a₁⋅──(y(t)) + b₀⋅x(t)
                  dt
The input and output symbols can be changed with the `inputsym` and `outputsym` arguments.

The response due to intial conditions is found with the `initial_response()` method:

   >>> fil.initial_response(('y0', ))
          ⎛           -a₁⋅t      ⎞
          ⎜           ──────     ⎟
          ⎜             a₀       ⎟
          ⎜δ(t)   a₁⋅ℯ      ⋅u(t)⎟
   -a₁⋅y₀⋅⎜──── - ───────────────⎟
          ⎜ a₀            2      ⎟
          ⎝             a₀       ⎠


An LTI filter can also be constructed from its transfer function in
zero-pole-gain representation with the `LTIFilter.from_ZPK` class
method.  For example,

    >>> fil = LTIFilter.from_ZPK(('z1',),('p1','p2'), 'K')
    >>> fil.a
    (1, -p₁ - p₂, p₁⋅p₂)
    >>> fil.b
    (K, -K⋅z₁)

A continuous-time LTI filter can be approximated by discrete-time LTI
filter using the `discretize()` method:




Continuous-time linear time invariant filter attributes
-------------------------------------------------------

- `a` denominator coefficients as a list
- `b` numerator coefficients as a list
- `is_marginally_stable` True if impulse response marginally stable
- `is_stable` True if impulse response stable


Continuous-time linear time invariant filter methods
----------------------------------------------------

- `differential_equation()` creates continuous-time differential equation
- `impulse_response()` creates continuous-time domain impulse response
- `step_response()` returns continuous time-domain step response
- `initial_response()` returns continuous time-domain response due to initial conditions
- `response()` returns continuous time-domain response due to input signal and initial conditions
- `transfer_fnction()` creates s-domain transfer function
- `sdomain_initial_response()` returns s-domain response due to initial conditions
- `group_delay()` returns the group delay as function of frequency, `f`


Butterworth filters
-------------------

Butterworth filters are created with the `Butterworth` class method.  For example::

  >>> B = Butterworth(order=2, Wn=omega0, btype='lowpass')
  >>> B.transfer_function()
            2
          ω₀
   ──────────────────
     2              2
   ω₀  + √2⋅ω₀⋅s + s


   >>> abs(B.frequency_response())
             2
           ω₀
   ───────────────────
      ________________
     ╱     4  4     4
   ╲╱  16⋅π ⋅f  + ω₀


   >>> B.group_delay()
         2  2           3
   4⋅√2⋅π ⋅f ⋅ω₀ + √2⋅ω₀
   ──────────────────────
           4  4     4
       16⋅π ⋅f  + ω₀



Bessel filters
--------------

Bessel filters are created with the `Bessel` class method.  For example::

  >>> B = Bessel(order=2, Wn=omega0, btype='lowpass')
  >>> B.transfer_function()
              2
          3⋅ω₀
   ───────────────────
       2             2
   3⋅ω₀  + 3⋅ω₀⋅s + s

   >>> abs(B.frequency_response())
                   2
               3⋅ω₀
   ──────────────────────────────
        2  2                    2
   - 4⋅π ⋅f  + 6⋅ⅉ⋅π⋅f⋅ω₀ + 3⋅ω₀

   >>> B.group_delay()
             2  2          3
         12⋅π ⋅f ⋅ω₀ + 9⋅ω₀
   ───────────────────────────────
       4  4       2  2   2       4
   16⋅π ⋅f  + 12⋅π ⋅f ⋅ω₀  + 9⋅ω₀


Differential equations
======================

Differential equations are represented by the `DifferentialEquation`
class.  They are usually created by the `differential_equation()`
method of the `LTIFilter` class or from transfer functions.  For example::

   >>> fil = LTIFilter(('b0', ), ('a0', 'a1'))
   >>> de = fil.differential_equation()
   >>> de
                  d
   a₀⋅y(t) = - a₁⋅──(y(t)) + b₀⋅x(t)
                  dt

There are two attributes: `lhs` for the left-hand-side and `rhs` for
the right-hand-side,

   >>> de.lhs
   a₀⋅y(t)
   >>> de.rhs
        d
   - a₁⋅──(y(t)) + b₀⋅x(t)
        dt

A transfer function is created with the `transfer_function()` method::

  >>> de.transfer_function()
      b₀
   ─────────
   a₀ + a₁⋅s

An `LTFilter` object is created with the `lti_filter()` method::

  >>> fil = de.lti_filter()


Differential equation attributes
--------------------------------

- `lhs` left-hand-side of the equation
- `rhs` right-hand-side of the equation
- `inputsym` input symbol, usually 'x'
- `outputsym` input symbol, usually 'y'


Differential equation methods
-----------------------------

- `dlti_filter()` creates continuous-time linear time invariant filter (`LTIFilter`) object
- `separate()` separates the input expressions from the output expressions.
- `transfer_function()` creates s-domain transfer function


.. _state-space:


Continuous-time state-space representation
==========================================

Lcapy has two state-space representations: `StateSpace` for
continuous-time linear time-invariant systems and `DTStateSpace` for
discrete-time linear time-invariant systems.  Both representations
share many methods and attributes, see :ref:`state-space-analysis`.

A state-space object is created from the state matrix, `A`, input
matrix, `B`, output matrix `C`, and feed-through matrix `D`::

    >>> ss = StateSpace(A, B, C, D)

A state-space object can also be created from lists of the numerator
and denominator coefficients `b` and `a`::

   >>> ss = StateSpace.from_ba(b, a)

By default, the controllable canonical form CCF is created.  The
observable canonical form OCF is created with::

   >>> ss = StateSpace.from_ba(b, a, form='OCF')

Similarly, the diagonal canonical form DCF is created with::

   >>> ss = StateSpace.from_ba(b, a, form='DCF')

For the DCF, the poles of the transfer function must be unique.


State-space from transfer function
----------------------------------

Transfer functions (and impedances and admittances) can be converted
to a state-space representation.  Here's an example::

   >>> Z = (s**2 + a) / (s**3 + b * s + c)
   >>> ss = Z.state_space('CCF')

State-space representation are not unique; Lcapy uses the controllable
canonical form (CCF), the observable canonical form (OCF), and the
diagonal canonical form (DCF).  The CCF form of the state-space
matrices are::

   >>> ss.A
   ⎡0   1   0⎤
   ⎢         ⎥
   ⎢0   0   1⎥
   ⎢         ⎥
   ⎣-c  -b  0⎦

   >>> ss.B
   ⎡0⎤
   ⎢ ⎥
   ⎢0⎥
   ⎢ ⎥
   ⎣1⎦

   >>> ss.C
   [a  0  1]

   >>> ss.D
   [0]


Transfer function from state-space
----------------------------------

For a single-input single-output (SISO) system the transfer function
is obtained with the `transfer_function()` method, for example::

   >>> ss = StateSpace(A, B, C, D)
   >>> G = ss.transfer_function


State-space operations
======================


Model balancing
---------------

This returns a new StateSpace object that has the controllability and
observability gramians equal to the diagonal matrix with the
Hankel singular values on the diagonal.  For example::

   >>> ss2 = ss.balance()

Note, this requires numerical A, B, C, D matrices.


Model reduction
---------------

A balanced reduction can be performed using::

   >>> ss2 = ss.balance_reduce(threshold=0.1)

where states are removed with a Hankel singular value below the
threshold.   Note, this requires numerical A, B, C, D matrices.

Alternatively, specific states can be removed.  For example::

     >>> ss2 = ss.reduce(elim_states=[1, 3])



.. _discrete_time_systems:

=====================
Discrete-time systems
=====================


Difference equations
====================

Difference equations can be generated from transfer functions and
impulse responses.  Both FIR and IIR (direct form I) can be generated.
For example::

  >>> H = (z + 2) / z**2
  >>> H.difference_equation('x', 'y', 'fir')
  y(n) = 2⋅x(n - 2) + x(n - 1)

Difference equations can be created explicitly, for example::

  >>> de = difference_equation('y(n)', '2 * x(n - 2) + x(n - 1)')

The `separate()` method separates the input expressions from the
output expressions.   For example::

  >>> de = difference_equation('y(n)', '2 * y(n - 1) + x(n)')
  >>> de.separate()
  y(n) - 2⋅y(n - 1) = x(n)


Difference equation attributes
------------------------------

- `lhs` left-hand-side of the equation
- `rhs` right-hand-side of the equation
- `inputsym` input symbol, usually 'x'
- `outputsym` input symbol, usually 'y'


Difference equation methods
---------------------------

- `dlti_filter()` creates discrete-time linear time invariant filter (`DLTIFilter`) object
- `separate()` separates the input expressions from the output expressions.
- `transfer_function()` creates z-domain transfer function


Discrete-time transfer functions
================================

A discrete-time transfer functions can be determined from a difference
equation or a DLTI filter.  For example::

   >>> de = difference_equation('y(n)', '2 * x(n - 2) + x(n - 1)')
   >>> H = de.transfer_function()
   >>> H
   z + 2
   ─────
     2
    z


Discrete-time transfer function methods
---------------------------------------

- `dlti_filter()` creates discrete-time linear time invariant filter (`DLTIFilter`) object
- `difference_equation()` creates discrete-time difference equation


.. _DLTIfilter:

Discrete-time linear time invariant filters
===========================================

A discrete-time linear time invariant filter can be specified by its
numerator and denominator coefficients.  For example, a first-order,
discrete-time, recursive low-pass filter can be created with:

   >>> a = symbol('a')
   >>> lpf = DLTIFilter((1 - a, ), (1, -a))

The difference equation can be printed using::

   >>> lpf.difference_equation()
   y(n) = a⋅y(n - 1) + (1 - a)⋅x(n)

The transfer function can be printed using::

   >>> lpf.transfer_function()
   z⋅(a - 1)
   ─────────
     a - z

The impulse response can be printed using::

   >>> lpf.impulse_response()
    n
   a ⋅(1 - a)⋅u[n]

The general response to an input `x(n)` can be printed using::

  >>> lpf.response(x, ni=(0, 5))

For a recursive filter, the initial conditions can also be specified::

  >>> lpf.response(x, ic=[1], ni=(0, 5))

The input to the filter can be a `DiscreteTimeDomainExpression` or a sequence.
The output is a sequence.

A discrete-time LTI filter can be created from difference equations
and transfer functions.   For example::

  >>> de = DifferenceEquation('2 * y(n)', '4 * y(n + 1) - 3 * y(n-3) -2 * x(n) - 5 * x(n-3)')
  >>> fil = de.dlti_filter()
  >>> fil.a
  [4, -2, 0, 0, -3]
  >>> fil.b
  [0, 2, 0, 0, 5]
  >>> fil.difference_equation()


Discrete-time linear time invariant filter attributes
-----------------------------------------------------

- `a` denominator coefficients as a list
- `b` numerator coefficients as a list
- `is_marginally_stable` True if impulse response marginally stable
- `is_stable` True if impulse response stable


Discrete-time linear time invariant filter methods
--------------------------------------------------

- `difference_equation()` creates discrete-time difference equation
- `impulse_response()` creates discrete-time domain impulse response
- `initial_response()` returns discrete time-domain response due to initial conditions
- `inverse()` creates an inverse filter by switching numerator and denominator coefficients
- `response()` returns discrete time-domain response due to input signal and initial conditions
- `transfer_function()` creates z-domain transfer function
- `zdomain_initial_response()` returns z-domain response due to initial conditions



Discrete-time state-space representation
========================================

Discrete-time state-space objects are defined in a similar manner to
continuous-time state-space objects and share many methods and
attributes.  A discrete-time state-space object is created from the
state matrix, `A`, input matrix, `B`, output matrix `C`, and
feed-through matrix `D`::

    >>> ss = DTStateSpace(A, B, C, D)

A state-space object can also be created from lists of the numerator
and denominator coefficients `b` and `a`::

   >>> ss = DTStateSpace.from_ba(b, a)

By default, the controllable canonical form CCF is created.  The
observable canonical form OCF is created with::

   >>> ss = DTStateSpace.from_ba(b, a, form='OCF')

Similarly, the diagonal canonical form DCF is created with::

   >>> ss = DTStateSpace.from_ba(b, a, form='DCF')

For the DCF, the poles of the transfer function must be unique.


For example::

   >>> ss = DTStateSpace(((0, 1), (1, 0)), (1, 1), ((1, 2), ), [1])

   >>> ss.A
   ⎡0  1⎤
   ⎢    ⎥
   ⎣1  0⎦

   >>> ss.B
   ⎡1⎤
   ⎢ ⎥
   ⎣1⎦

   >>> ss.C
   [1  2]

   >>> ss.D
   [1]

   >>> ss.state_equations()
   ⎡x₀(n + 1)⎤   ⎡0  1⎤ ⎡x₀(n)⎤   ⎡1⎤
   ⎢         ⎥ = ⎢    ⎥⋅⎢     ⎥ + ⎢ ⎥⋅[u₀(n)]
   ⎣x₁(n + 1)⎦   ⎣1  0⎦ ⎣x₁(n)⎦   ⎣1⎦

   >>> ss.output_equations()
                    ⎡x₀(n)⎤
   [y₀(n)] = [1  2]⋅⎢     ⎥ + [1]⋅[u₀(n)]
                    ⎣x₁(n)⎦



   >>> ss.controllability_matrix
   ⎡1  1⎤
   ⎢    ⎥
   ⎣1  1⎦

   >>> ss.is_controllable
   False


   >>> ss = DTStateSpace(((0, 1), (1, 1)), (1, 1), ((1, 2), ), [1])

   >>> ss.A
   ⎡0  1⎤
   ⎢    ⎥
   ⎣1  1⎦

   >>> ss.B
   ⎡1⎤
   ⎢ ⎥
   ⎣1⎦

   >>> ss.C
   [1  2]

   >>> ss.D
   [1]

   >>> ss.is_stable
   False

   >>> ss.eigenvalues
   [-1, 1]

   >>> ss.controllability_matrix
   ⎡1  1⎤
   ⎢    ⎥
   ⎣1  2⎦

   >>> ss.is_controllable
   True

   >>> ss.is_observable
   True

   >>> ss.state_transfer([[2], [3]], xinitial=[0, 0])
   ⎡5⎤
   ⎢ ⎥
   ⎣7⎦

   >>> ss.minimum_energy_input(2, [5, 7], [0, 0])
   ⎡2⎤
   ⎢ ⎥
   ⎣3⎦

   >>> ss.minimum_energy(2, [5, 7], [0, 0])

   >>> ss.minimum_energy_input(3, [5, 7], [0, 0])
   ⎡5/3⎤
   ⎢   ⎥
   ⎢1/3⎥
   ⎢   ⎥
   ⎣4/3⎦

   >>> ss.minimum_energy(3, [5, 7], [0, 0])
   14/3
