# 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

## 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.

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

```

### 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¶

## 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

## 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=, 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), ), )

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

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

>>> ss.C
[1  2]

>>> ss.D


>>> 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]⋅⎢     ⎥ + ⋅[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), ), )

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

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

>>> ss.C
[1  2]

>>> ss.D


>>> 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([, ], 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
```