Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/scipy/signal/ltisys.py : 15%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2ltisys -- a collection of classes and functions for modeling linear
3time invariant systems.
4"""
5#
6# Author: Travis Oliphant 2001
7#
8# Feb 2010: Warren Weckesser
9# Rewrote lsim2 and added impulse2.
10# Apr 2011: Jeffrey Armstrong <jeff@approximatrix.com>
11# Added dlsim, dstep, dimpulse, cont2discrete
12# Aug 2013: Juan Luis Cano
13# Rewrote abcd_normalize.
14# Jan 2015: Irvin Probst irvin DOT probst AT ensta-bretagne DOT fr
15# Added pole placement
16# Mar 2015: Clancy Rowley
17# Rewrote lsim
18# May 2015: Felix Berkenkamp
19# Split lti class into subclasses
20# Merged discrete systems and added dlti
22import warnings
24# np.linalg.qr fails on some tests with LinAlgError: zgeqrf returns -7
25# use scipy's qr until this is solved
27from scipy.linalg import qr as s_qr
28from scipy import integrate, interpolate, linalg
29from scipy.interpolate import interp1d
30from .filter_design import (tf2zpk, zpk2tf, normalize, freqs, freqz, freqs_zpk,
31 freqz_zpk)
32from .lti_conversion import (tf2ss, abcd_normalize, ss2tf, zpk2ss, ss2zpk,
33 cont2discrete)
35import numpy
36import numpy as np
37from numpy import (real, atleast_1d, atleast_2d, squeeze, asarray, zeros,
38 dot, transpose, ones, zeros_like, linspace, nan_to_num)
39import copy
41__all__ = ['lti', 'dlti', 'TransferFunction', 'ZerosPolesGain', 'StateSpace',
42 'lsim', 'lsim2', 'impulse', 'impulse2', 'step', 'step2', 'bode',
43 'freqresp', 'place_poles', 'dlsim', 'dstep', 'dimpulse',
44 'dfreqresp', 'dbode']
47class LinearTimeInvariant(object):
48 def __new__(cls, *system, **kwargs):
49 """Create a new object, don't allow direct instances."""
50 if cls is LinearTimeInvariant:
51 raise NotImplementedError('The LinearTimeInvariant class is not '
52 'meant to be used directly, use `lti` '
53 'or `dlti` instead.')
54 return super(LinearTimeInvariant, cls).__new__(cls)
56 def __init__(self):
57 """
58 Initialize the `lti` baseclass.
60 The heavy lifting is done by the subclasses.
61 """
62 super(LinearTimeInvariant, self).__init__()
64 self.inputs = None
65 self.outputs = None
66 self._dt = None
68 @property
69 def dt(self):
70 """Return the sampling time of the system, `None` for `lti` systems."""
71 return self._dt
73 @property
74 def _dt_dict(self):
75 if self.dt is None:
76 return {}
77 else:
78 return {'dt': self.dt}
80 @property
81 def zeros(self):
82 """Zeros of the system."""
83 return self.to_zpk().zeros
85 @property
86 def poles(self):
87 """Poles of the system."""
88 return self.to_zpk().poles
90 def _as_ss(self):
91 """Convert to `StateSpace` system, without copying.
93 Returns
94 -------
95 sys: StateSpace
96 The `StateSpace` system. If the class is already an instance of
97 `StateSpace` then this instance is returned.
98 """
99 if isinstance(self, StateSpace):
100 return self
101 else:
102 return self.to_ss()
104 def _as_zpk(self):
105 """Convert to `ZerosPolesGain` system, without copying.
107 Returns
108 -------
109 sys: ZerosPolesGain
110 The `ZerosPolesGain` system. If the class is already an instance of
111 `ZerosPolesGain` then this instance is returned.
112 """
113 if isinstance(self, ZerosPolesGain):
114 return self
115 else:
116 return self.to_zpk()
118 def _as_tf(self):
119 """Convert to `TransferFunction` system, without copying.
121 Returns
122 -------
123 sys: ZerosPolesGain
124 The `TransferFunction` system. If the class is already an instance of
125 `TransferFunction` then this instance is returned.
126 """
127 if isinstance(self, TransferFunction):
128 return self
129 else:
130 return self.to_tf()
133class lti(LinearTimeInvariant):
134 """
135 Continuous-time linear time invariant system base class.
137 Parameters
138 ----------
139 *system : arguments
140 The `lti` class can be instantiated with either 2, 3 or 4 arguments.
141 The following gives the number of arguments and the corresponding
142 continuous-time subclass that is created:
144 * 2: `TransferFunction`: (numerator, denominator)
145 * 3: `ZerosPolesGain`: (zeros, poles, gain)
146 * 4: `StateSpace`: (A, B, C, D)
148 Each argument can be an array or a sequence.
150 See Also
151 --------
152 ZerosPolesGain, StateSpace, TransferFunction, dlti
154 Notes
155 -----
156 `lti` instances do not exist directly. Instead, `lti` creates an instance
157 of one of its subclasses: `StateSpace`, `TransferFunction` or
158 `ZerosPolesGain`.
160 If (numerator, denominator) is passed in for ``*system``, coefficients for
161 both the numerator and denominator should be specified in descending
162 exponent order (e.g., ``s^2 + 3s + 5`` would be represented as ``[1, 3,
163 5]``).
165 Changing the value of properties that are not directly part of the current
166 system representation (such as the `zeros` of a `StateSpace` system) is
167 very inefficient and may lead to numerical inaccuracies. It is better to
168 convert to the specific system representation first. For example, call
169 ``sys = sys.to_zpk()`` before accessing/changing the zeros, poles or gain.
171 Examples
172 --------
173 >>> from scipy import signal
175 >>> signal.lti(1, 2, 3, 4)
176 StateSpaceContinuous(
177 array([[1]]),
178 array([[2]]),
179 array([[3]]),
180 array([[4]]),
181 dt: None
182 )
184 >>> signal.lti([1, 2], [3, 4], 5)
185 ZerosPolesGainContinuous(
186 array([1, 2]),
187 array([3, 4]),
188 5,
189 dt: None
190 )
192 >>> signal.lti([3, 4], [1, 2])
193 TransferFunctionContinuous(
194 array([3., 4.]),
195 array([1., 2.]),
196 dt: None
197 )
199 """
200 def __new__(cls, *system):
201 """Create an instance of the appropriate subclass."""
202 if cls is lti:
203 N = len(system)
204 if N == 2:
205 return TransferFunctionContinuous.__new__(
206 TransferFunctionContinuous, *system)
207 elif N == 3:
208 return ZerosPolesGainContinuous.__new__(
209 ZerosPolesGainContinuous, *system)
210 elif N == 4:
211 return StateSpaceContinuous.__new__(StateSpaceContinuous,
212 *system)
213 else:
214 raise ValueError("`system` needs to be an instance of `lti` "
215 "or have 2, 3 or 4 arguments.")
216 # __new__ was called from a subclass, let it call its own functions
217 return super(lti, cls).__new__(cls)
219 def __init__(self, *system):
220 """
221 Initialize the `lti` baseclass.
223 The heavy lifting is done by the subclasses.
224 """
225 super(lti, self).__init__(*system)
227 def impulse(self, X0=None, T=None, N=None):
228 """
229 Return the impulse response of a continuous-time system.
230 See `impulse` for details.
231 """
232 return impulse(self, X0=X0, T=T, N=N)
234 def step(self, X0=None, T=None, N=None):
235 """
236 Return the step response of a continuous-time system.
237 See `step` for details.
238 """
239 return step(self, X0=X0, T=T, N=N)
241 def output(self, U, T, X0=None):
242 """
243 Return the response of a continuous-time system to input `U`.
244 See `lsim` for details.
245 """
246 return lsim(self, U, T, X0=X0)
248 def bode(self, w=None, n=100):
249 """
250 Calculate Bode magnitude and phase data of a continuous-time system.
252 Returns a 3-tuple containing arrays of frequencies [rad/s], magnitude
253 [dB] and phase [deg]. See `bode` for details.
255 Examples
256 --------
257 >>> from scipy import signal
258 >>> import matplotlib.pyplot as plt
260 >>> sys = signal.TransferFunction([1], [1, 1])
261 >>> w, mag, phase = sys.bode()
263 >>> plt.figure()
264 >>> plt.semilogx(w, mag) # Bode magnitude plot
265 >>> plt.figure()
266 >>> plt.semilogx(w, phase) # Bode phase plot
267 >>> plt.show()
269 """
270 return bode(self, w=w, n=n)
272 def freqresp(self, w=None, n=10000):
273 """
274 Calculate the frequency response of a continuous-time system.
276 Returns a 2-tuple containing arrays of frequencies [rad/s] and
277 complex magnitude.
278 See `freqresp` for details.
279 """
280 return freqresp(self, w=w, n=n)
282 def to_discrete(self, dt, method='zoh', alpha=None):
283 """Return a discretized version of the current system.
285 Parameters: See `cont2discrete` for details.
287 Returns
288 -------
289 sys: instance of `dlti`
290 """
291 raise NotImplementedError('to_discrete is not implemented for this '
292 'system class.')
295class dlti(LinearTimeInvariant):
296 """
297 Discrete-time linear time invariant system base class.
299 Parameters
300 ----------
301 *system: arguments
302 The `dlti` class can be instantiated with either 2, 3 or 4 arguments.
303 The following gives the number of arguments and the corresponding
304 discrete-time subclass that is created:
306 * 2: `TransferFunction`: (numerator, denominator)
307 * 3: `ZerosPolesGain`: (zeros, poles, gain)
308 * 4: `StateSpace`: (A, B, C, D)
310 Each argument can be an array or a sequence.
311 dt: float, optional
312 Sampling time [s] of the discrete-time systems. Defaults to ``True``
313 (unspecified sampling time). Must be specified as a keyword argument,
314 for example, ``dt=0.1``.
316 See Also
317 --------
318 ZerosPolesGain, StateSpace, TransferFunction, lti
320 Notes
321 -----
322 `dlti` instances do not exist directly. Instead, `dlti` creates an instance
323 of one of its subclasses: `StateSpace`, `TransferFunction` or
324 `ZerosPolesGain`.
326 Changing the value of properties that are not directly part of the current
327 system representation (such as the `zeros` of a `StateSpace` system) is
328 very inefficient and may lead to numerical inaccuracies. It is better to
329 convert to the specific system representation first. For example, call
330 ``sys = sys.to_zpk()`` before accessing/changing the zeros, poles or gain.
332 If (numerator, denominator) is passed in for ``*system``, coefficients for
333 both the numerator and denominator should be specified in descending
334 exponent order (e.g., ``z^2 + 3z + 5`` would be represented as ``[1, 3,
335 5]``).
337 .. versionadded:: 0.18.0
339 Examples
340 --------
341 >>> from scipy import signal
343 >>> signal.dlti(1, 2, 3, 4)
344 StateSpaceDiscrete(
345 array([[1]]),
346 array([[2]]),
347 array([[3]]),
348 array([[4]]),
349 dt: True
350 )
352 >>> signal.dlti(1, 2, 3, 4, dt=0.1)
353 StateSpaceDiscrete(
354 array([[1]]),
355 array([[2]]),
356 array([[3]]),
357 array([[4]]),
358 dt: 0.1
359 )
361 >>> signal.dlti([1, 2], [3, 4], 5, dt=0.1)
362 ZerosPolesGainDiscrete(
363 array([1, 2]),
364 array([3, 4]),
365 5,
366 dt: 0.1
367 )
369 >>> signal.dlti([3, 4], [1, 2], dt=0.1)
370 TransferFunctionDiscrete(
371 array([3., 4.]),
372 array([1., 2.]),
373 dt: 0.1
374 )
376 """
377 def __new__(cls, *system, **kwargs):
378 """Create an instance of the appropriate subclass."""
379 if cls is dlti:
380 N = len(system)
381 if N == 2:
382 return TransferFunctionDiscrete.__new__(
383 TransferFunctionDiscrete, *system, **kwargs)
384 elif N == 3:
385 return ZerosPolesGainDiscrete.__new__(ZerosPolesGainDiscrete,
386 *system, **kwargs)
387 elif N == 4:
388 return StateSpaceDiscrete.__new__(StateSpaceDiscrete, *system,
389 **kwargs)
390 else:
391 raise ValueError("`system` needs to be an instance of `dlti` "
392 "or have 2, 3 or 4 arguments.")
393 # __new__ was called from a subclass, let it call its own functions
394 return super(dlti, cls).__new__(cls)
396 def __init__(self, *system, **kwargs):
397 """
398 Initialize the `lti` baseclass.
400 The heavy lifting is done by the subclasses.
401 """
402 dt = kwargs.pop('dt', True)
403 super(dlti, self).__init__(*system, **kwargs)
405 self.dt = dt
407 @property
408 def dt(self):
409 """Return the sampling time of the system."""
410 return self._dt
412 @dt.setter
413 def dt(self, dt):
414 self._dt = dt
416 def impulse(self, x0=None, t=None, n=None):
417 """
418 Return the impulse response of the discrete-time `dlti` system.
419 See `dimpulse` for details.
420 """
421 return dimpulse(self, x0=x0, t=t, n=n)
423 def step(self, x0=None, t=None, n=None):
424 """
425 Return the step response of the discrete-time `dlti` system.
426 See `dstep` for details.
427 """
428 return dstep(self, x0=x0, t=t, n=n)
430 def output(self, u, t, x0=None):
431 """
432 Return the response of the discrete-time system to input `u`.
433 See `dlsim` for details.
434 """
435 return dlsim(self, u, t, x0=x0)
437 def bode(self, w=None, n=100):
438 """
439 Calculate Bode magnitude and phase data of a discrete-time system.
441 Returns a 3-tuple containing arrays of frequencies [rad/s], magnitude
442 [dB] and phase [deg]. See `dbode` for details.
444 Examples
445 --------
446 >>> from scipy import signal
447 >>> import matplotlib.pyplot as plt
449 Transfer function: H(z) = 1 / (z^2 + 2z + 3) with sampling time 0.5s
451 >>> sys = signal.TransferFunction([1], [1, 2, 3], dt=0.5)
453 Equivalent: signal.dbode(sys)
455 >>> w, mag, phase = sys.bode()
457 >>> plt.figure()
458 >>> plt.semilogx(w, mag) # Bode magnitude plot
459 >>> plt.figure()
460 >>> plt.semilogx(w, phase) # Bode phase plot
461 >>> plt.show()
463 """
464 return dbode(self, w=w, n=n)
466 def freqresp(self, w=None, n=10000, whole=False):
467 """
468 Calculate the frequency response of a discrete-time system.
470 Returns a 2-tuple containing arrays of frequencies [rad/s] and
471 complex magnitude.
472 See `dfreqresp` for details.
474 """
475 return dfreqresp(self, w=w, n=n, whole=whole)
478class TransferFunction(LinearTimeInvariant):
479 r"""Linear Time Invariant system class in transfer function form.
481 Represents the system as the continuous-time transfer function
482 :math:`H(s)=\sum_{i=0}^N b[N-i] s^i / \sum_{j=0}^M a[M-j] s^j` or the
483 discrete-time transfer function
484 :math:`H(s)=\sum_{i=0}^N b[N-i] z^i / \sum_{j=0}^M a[M-j] z^j`, where
485 :math:`b` are elements of the numerator `num`, :math:`a` are elements of
486 the denominator `den`, and ``N == len(b) - 1``, ``M == len(a) - 1``.
487 `TransferFunction` systems inherit additional
488 functionality from the `lti`, respectively the `dlti` classes, depending on
489 which system representation is used.
491 Parameters
492 ----------
493 *system: arguments
494 The `TransferFunction` class can be instantiated with 1 or 2
495 arguments. The following gives the number of input arguments and their
496 interpretation:
498 * 1: `lti` or `dlti` system: (`StateSpace`, `TransferFunction` or
499 `ZerosPolesGain`)
500 * 2: array_like: (numerator, denominator)
501 dt: float, optional
502 Sampling time [s] of the discrete-time systems. Defaults to `None`
503 (continuous-time). Must be specified as a keyword argument, for
504 example, ``dt=0.1``.
506 See Also
507 --------
508 ZerosPolesGain, StateSpace, lti, dlti
509 tf2ss, tf2zpk, tf2sos
511 Notes
512 -----
513 Changing the value of properties that are not part of the
514 `TransferFunction` system representation (such as the `A`, `B`, `C`, `D`
515 state-space matrices) is very inefficient and may lead to numerical
516 inaccuracies. It is better to convert to the specific system
517 representation first. For example, call ``sys = sys.to_ss()`` before
518 accessing/changing the A, B, C, D system matrices.
520 If (numerator, denominator) is passed in for ``*system``, coefficients
521 for both the numerator and denominator should be specified in descending
522 exponent order (e.g. ``s^2 + 3s + 5`` or ``z^2 + 3z + 5`` would be
523 represented as ``[1, 3, 5]``)
525 Examples
526 --------
527 Construct the transfer function:
529 .. math:: H(s) = \frac{s^2 + 3s + 3}{s^2 + 2s + 1}
531 >>> from scipy import signal
533 >>> num = [1, 3, 3]
534 >>> den = [1, 2, 1]
536 >>> signal.TransferFunction(num, den)
537 TransferFunctionContinuous(
538 array([1., 3., 3.]),
539 array([1., 2., 1.]),
540 dt: None
541 )
543 Construct the transfer function with a sampling time of 0.1 seconds:
545 .. math:: H(z) = \frac{z^2 + 3z + 3}{z^2 + 2z + 1}
547 >>> signal.TransferFunction(num, den, dt=0.1)
548 TransferFunctionDiscrete(
549 array([1., 3., 3.]),
550 array([1., 2., 1.]),
551 dt: 0.1
552 )
554 """
555 def __new__(cls, *system, **kwargs):
556 """Handle object conversion if input is an instance of lti."""
557 if len(system) == 1 and isinstance(system[0], LinearTimeInvariant):
558 return system[0].to_tf()
560 # Choose whether to inherit from `lti` or from `dlti`
561 if cls is TransferFunction:
562 if kwargs.get('dt') is None:
563 return TransferFunctionContinuous.__new__(
564 TransferFunctionContinuous,
565 *system,
566 **kwargs)
567 else:
568 return TransferFunctionDiscrete.__new__(
569 TransferFunctionDiscrete,
570 *system,
571 **kwargs)
573 # No special conversion needed
574 return super(TransferFunction, cls).__new__(cls)
576 def __init__(self, *system, **kwargs):
577 """Initialize the state space LTI system."""
578 # Conversion of lti instances is handled in __new__
579 if isinstance(system[0], LinearTimeInvariant):
580 return
582 # Remove system arguments, not needed by parents anymore
583 super(TransferFunction, self).__init__(**kwargs)
585 self._num = None
586 self._den = None
588 self.num, self.den = normalize(*system)
590 def __repr__(self):
591 """Return representation of the system's transfer function"""
592 return '{0}(\n{1},\n{2},\ndt: {3}\n)'.format(
593 self.__class__.__name__,
594 repr(self.num),
595 repr(self.den),
596 repr(self.dt),
597 )
599 @property
600 def num(self):
601 """Numerator of the `TransferFunction` system."""
602 return self._num
604 @num.setter
605 def num(self, num):
606 self._num = atleast_1d(num)
608 # Update dimensions
609 if len(self.num.shape) > 1:
610 self.outputs, self.inputs = self.num.shape
611 else:
612 self.outputs = 1
613 self.inputs = 1
615 @property
616 def den(self):
617 """Denominator of the `TransferFunction` system."""
618 return self._den
620 @den.setter
621 def den(self, den):
622 self._den = atleast_1d(den)
624 def _copy(self, system):
625 """
626 Copy the parameters of another `TransferFunction` object
628 Parameters
629 ----------
630 system : `TransferFunction`
631 The `StateSpace` system that is to be copied
633 """
634 self.num = system.num
635 self.den = system.den
637 def to_tf(self):
638 """
639 Return a copy of the current `TransferFunction` system.
641 Returns
642 -------
643 sys : instance of `TransferFunction`
644 The current system (copy)
646 """
647 return copy.deepcopy(self)
649 def to_zpk(self):
650 """
651 Convert system representation to `ZerosPolesGain`.
653 Returns
654 -------
655 sys : instance of `ZerosPolesGain`
656 Zeros, poles, gain representation of the current system
658 """
659 return ZerosPolesGain(*tf2zpk(self.num, self.den),
660 **self._dt_dict)
662 def to_ss(self):
663 """
664 Convert system representation to `StateSpace`.
666 Returns
667 -------
668 sys : instance of `StateSpace`
669 State space model of the current system
671 """
672 return StateSpace(*tf2ss(self.num, self.den),
673 **self._dt_dict)
675 @staticmethod
676 def _z_to_zinv(num, den):
677 """Change a transfer function from the variable `z` to `z**-1`.
679 Parameters
680 ----------
681 num, den: 1d array_like
682 Sequences representing the coefficients of the numerator and
683 denominator polynomials, in order of descending degree of 'z'.
684 That is, ``5z**2 + 3z + 2`` is presented as ``[5, 3, 2]``.
686 Returns
687 -------
688 num, den: 1d array_like
689 Sequences representing the coefficients of the numerator and
690 denominator polynomials, in order of ascending degree of 'z**-1'.
691 That is, ``5 + 3 z**-1 + 2 z**-2`` is presented as ``[5, 3, 2]``.
692 """
693 diff = len(num) - len(den)
694 if diff > 0:
695 den = np.hstack((np.zeros(diff), den))
696 elif diff < 0:
697 num = np.hstack((np.zeros(-diff), num))
698 return num, den
700 @staticmethod
701 def _zinv_to_z(num, den):
702 """Change a transfer function from the variable `z` to `z**-1`.
704 Parameters
705 ----------
706 num, den: 1d array_like
707 Sequences representing the coefficients of the numerator and
708 denominator polynomials, in order of ascending degree of 'z**-1'.
709 That is, ``5 + 3 z**-1 + 2 z**-2`` is presented as ``[5, 3, 2]``.
711 Returns
712 -------
713 num, den: 1d array_like
714 Sequences representing the coefficients of the numerator and
715 denominator polynomials, in order of descending degree of 'z'.
716 That is, ``5z**2 + 3z + 2`` is presented as ``[5, 3, 2]``.
717 """
718 diff = len(num) - len(den)
719 if diff > 0:
720 den = np.hstack((den, np.zeros(diff)))
721 elif diff < 0:
722 num = np.hstack((num, np.zeros(-diff)))
723 return num, den
726class TransferFunctionContinuous(TransferFunction, lti):
727 r"""
728 Continuous-time Linear Time Invariant system in transfer function form.
730 Represents the system as the transfer function
731 :math:`H(s)=\sum_{i=0}^N b[N-i] s^i / \sum_{j=0}^M a[M-j] s^j`, where
732 :math:`b` are elements of the numerator `num`, :math:`a` are elements of
733 the denominator `den`, and ``N == len(b) - 1``, ``M == len(a) - 1``.
734 Continuous-time `TransferFunction` systems inherit additional
735 functionality from the `lti` class.
737 Parameters
738 ----------
739 *system: arguments
740 The `TransferFunction` class can be instantiated with 1 or 2
741 arguments. The following gives the number of input arguments and their
742 interpretation:
744 * 1: `lti` system: (`StateSpace`, `TransferFunction` or
745 `ZerosPolesGain`)
746 * 2: array_like: (numerator, denominator)
748 See Also
749 --------
750 ZerosPolesGain, StateSpace, lti
751 tf2ss, tf2zpk, tf2sos
753 Notes
754 -----
755 Changing the value of properties that are not part of the
756 `TransferFunction` system representation (such as the `A`, `B`, `C`, `D`
757 state-space matrices) is very inefficient and may lead to numerical
758 inaccuracies. It is better to convert to the specific system
759 representation first. For example, call ``sys = sys.to_ss()`` before
760 accessing/changing the A, B, C, D system matrices.
762 If (numerator, denominator) is passed in for ``*system``, coefficients
763 for both the numerator and denominator should be specified in descending
764 exponent order (e.g. ``s^2 + 3s + 5`` would be represented as
765 ``[1, 3, 5]``)
767 Examples
768 --------
769 Construct the transfer function:
771 .. math:: H(s) = \frac{s^2 + 3s + 3}{s^2 + 2s + 1}
773 >>> from scipy import signal
775 >>> num = [1, 3, 3]
776 >>> den = [1, 2, 1]
778 >>> signal.TransferFunction(num, den)
779 TransferFunctionContinuous(
780 array([ 1., 3., 3.]),
781 array([ 1., 2., 1.]),
782 dt: None
783 )
785 """
786 def to_discrete(self, dt, method='zoh', alpha=None):
787 """
788 Returns the discretized `TransferFunction` system.
790 Parameters: See `cont2discrete` for details.
792 Returns
793 -------
794 sys: instance of `dlti` and `StateSpace`
795 """
796 return TransferFunction(*cont2discrete((self.num, self.den),
797 dt,
798 method=method,
799 alpha=alpha)[:-1],
800 dt=dt)
803class TransferFunctionDiscrete(TransferFunction, dlti):
804 r"""
805 Discrete-time Linear Time Invariant system in transfer function form.
807 Represents the system as the transfer function
808 :math:`H(z)=\sum_{i=0}^N b[N-i] z^i / \sum_{j=0}^M a[M-j] z^j`, where
809 :math:`b` are elements of the numerator `num`, :math:`a` are elements of
810 the denominator `den`, and ``N == len(b) - 1``, ``M == len(a) - 1``.
811 Discrete-time `TransferFunction` systems inherit additional functionality
812 from the `dlti` class.
814 Parameters
815 ----------
816 *system: arguments
817 The `TransferFunction` class can be instantiated with 1 or 2
818 arguments. The following gives the number of input arguments and their
819 interpretation:
821 * 1: `dlti` system: (`StateSpace`, `TransferFunction` or
822 `ZerosPolesGain`)
823 * 2: array_like: (numerator, denominator)
824 dt: float, optional
825 Sampling time [s] of the discrete-time systems. Defaults to `True`
826 (unspecified sampling time). Must be specified as a keyword argument,
827 for example, ``dt=0.1``.
829 See Also
830 --------
831 ZerosPolesGain, StateSpace, dlti
832 tf2ss, tf2zpk, tf2sos
834 Notes
835 -----
836 Changing the value of properties that are not part of the
837 `TransferFunction` system representation (such as the `A`, `B`, `C`, `D`
838 state-space matrices) is very inefficient and may lead to numerical
839 inaccuracies.
841 If (numerator, denominator) is passed in for ``*system``, coefficients
842 for both the numerator and denominator should be specified in descending
843 exponent order (e.g., ``z^2 + 3z + 5`` would be represented as
844 ``[1, 3, 5]``).
846 Examples
847 --------
848 Construct the transfer function with a sampling time of 0.5 seconds:
850 .. math:: H(z) = \frac{z^2 + 3z + 3}{z^2 + 2z + 1}
852 >>> from scipy import signal
854 >>> num = [1, 3, 3]
855 >>> den = [1, 2, 1]
857 >>> signal.TransferFunction(num, den, 0.5)
858 TransferFunctionDiscrete(
859 array([ 1., 3., 3.]),
860 array([ 1., 2., 1.]),
861 dt: 0.5
862 )
864 """
865 pass
868class ZerosPolesGain(LinearTimeInvariant):
869 r"""
870 Linear Time Invariant system class in zeros, poles, gain form.
872 Represents the system as the continuous- or discrete-time transfer function
873 :math:`H(s)=k \prod_i (s - z[i]) / \prod_j (s - p[j])`, where :math:`k` is
874 the `gain`, :math:`z` are the `zeros` and :math:`p` are the `poles`.
875 `ZerosPolesGain` systems inherit additional functionality from the `lti`,
876 respectively the `dlti` classes, depending on which system representation
877 is used.
879 Parameters
880 ----------
881 *system : arguments
882 The `ZerosPolesGain` class can be instantiated with 1 or 3
883 arguments. The following gives the number of input arguments and their
884 interpretation:
886 * 1: `lti` or `dlti` system: (`StateSpace`, `TransferFunction` or
887 `ZerosPolesGain`)
888 * 3: array_like: (zeros, poles, gain)
889 dt: float, optional
890 Sampling time [s] of the discrete-time systems. Defaults to `None`
891 (continuous-time). Must be specified as a keyword argument, for
892 example, ``dt=0.1``.
895 See Also
896 --------
897 TransferFunction, StateSpace, lti, dlti
898 zpk2ss, zpk2tf, zpk2sos
900 Notes
901 -----
902 Changing the value of properties that are not part of the
903 `ZerosPolesGain` system representation (such as the `A`, `B`, `C`, `D`
904 state-space matrices) is very inefficient and may lead to numerical
905 inaccuracies. It is better to convert to the specific system
906 representation first. For example, call ``sys = sys.to_ss()`` before
907 accessing/changing the A, B, C, D system matrices.
909 Examples
910 --------
911 >>> from scipy import signal
913 Transfer function: H(s) = 5(s - 1)(s - 2) / (s - 3)(s - 4)
915 >>> signal.ZerosPolesGain([1, 2], [3, 4], 5)
916 ZerosPolesGainContinuous(
917 array([1, 2]),
918 array([3, 4]),
919 5,
920 dt: None
921 )
923 Transfer function: H(z) = 5(z - 1)(z - 2) / (z - 3)(z - 4)
925 >>> signal.ZerosPolesGain([1, 2], [3, 4], 5, dt=0.1)
926 ZerosPolesGainDiscrete(
927 array([1, 2]),
928 array([3, 4]),
929 5,
930 dt: 0.1
931 )
933 """
934 def __new__(cls, *system, **kwargs):
935 """Handle object conversion if input is an instance of `lti`"""
936 if len(system) == 1 and isinstance(system[0], LinearTimeInvariant):
937 return system[0].to_zpk()
939 # Choose whether to inherit from `lti` or from `dlti`
940 if cls is ZerosPolesGain:
941 if kwargs.get('dt') is None:
942 return ZerosPolesGainContinuous.__new__(
943 ZerosPolesGainContinuous,
944 *system,
945 **kwargs)
946 else:
947 return ZerosPolesGainDiscrete.__new__(
948 ZerosPolesGainDiscrete,
949 *system,
950 **kwargs
951 )
953 # No special conversion needed
954 return super(ZerosPolesGain, cls).__new__(cls)
956 def __init__(self, *system, **kwargs):
957 """Initialize the zeros, poles, gain system."""
958 # Conversion of lti instances is handled in __new__
959 if isinstance(system[0], LinearTimeInvariant):
960 return
962 super(ZerosPolesGain, self).__init__(**kwargs)
964 self._zeros = None
965 self._poles = None
966 self._gain = None
968 self.zeros, self.poles, self.gain = system
970 def __repr__(self):
971 """Return representation of the `ZerosPolesGain` system."""
972 return '{0}(\n{1},\n{2},\n{3},\ndt: {4}\n)'.format(
973 self.__class__.__name__,
974 repr(self.zeros),
975 repr(self.poles),
976 repr(self.gain),
977 repr(self.dt),
978 )
980 @property
981 def zeros(self):
982 """Zeros of the `ZerosPolesGain` system."""
983 return self._zeros
985 @zeros.setter
986 def zeros(self, zeros):
987 self._zeros = atleast_1d(zeros)
989 # Update dimensions
990 if len(self.zeros.shape) > 1:
991 self.outputs, self.inputs = self.zeros.shape
992 else:
993 self.outputs = 1
994 self.inputs = 1
996 @property
997 def poles(self):
998 """Poles of the `ZerosPolesGain` system."""
999 return self._poles
1001 @poles.setter
1002 def poles(self, poles):
1003 self._poles = atleast_1d(poles)
1005 @property
1006 def gain(self):
1007 """Gain of the `ZerosPolesGain` system."""
1008 return self._gain
1010 @gain.setter
1011 def gain(self, gain):
1012 self._gain = gain
1014 def _copy(self, system):
1015 """
1016 Copy the parameters of another `ZerosPolesGain` system.
1018 Parameters
1019 ----------
1020 system : instance of `ZerosPolesGain`
1021 The zeros, poles gain system that is to be copied
1023 """
1024 self.poles = system.poles
1025 self.zeros = system.zeros
1026 self.gain = system.gain
1028 def to_tf(self):
1029 """
1030 Convert system representation to `TransferFunction`.
1032 Returns
1033 -------
1034 sys : instance of `TransferFunction`
1035 Transfer function of the current system
1037 """
1038 return TransferFunction(*zpk2tf(self.zeros, self.poles, self.gain),
1039 **self._dt_dict)
1041 def to_zpk(self):
1042 """
1043 Return a copy of the current 'ZerosPolesGain' system.
1045 Returns
1046 -------
1047 sys : instance of `ZerosPolesGain`
1048 The current system (copy)
1050 """
1051 return copy.deepcopy(self)
1053 def to_ss(self):
1054 """
1055 Convert system representation to `StateSpace`.
1057 Returns
1058 -------
1059 sys : instance of `StateSpace`
1060 State space model of the current system
1062 """
1063 return StateSpace(*zpk2ss(self.zeros, self.poles, self.gain),
1064 **self._dt_dict)
1067class ZerosPolesGainContinuous(ZerosPolesGain, lti):
1068 r"""
1069 Continuous-time Linear Time Invariant system in zeros, poles, gain form.
1071 Represents the system as the continuous time transfer function
1072 :math:`H(s)=k \prod_i (s - z[i]) / \prod_j (s - p[j])`, where :math:`k` is
1073 the `gain`, :math:`z` are the `zeros` and :math:`p` are the `poles`.
1074 Continuous-time `ZerosPolesGain` systems inherit additional functionality
1075 from the `lti` class.
1077 Parameters
1078 ----------
1079 *system : arguments
1080 The `ZerosPolesGain` class can be instantiated with 1 or 3
1081 arguments. The following gives the number of input arguments and their
1082 interpretation:
1084 * 1: `lti` system: (`StateSpace`, `TransferFunction` or
1085 `ZerosPolesGain`)
1086 * 3: array_like: (zeros, poles, gain)
1088 See Also
1089 --------
1090 TransferFunction, StateSpace, lti
1091 zpk2ss, zpk2tf, zpk2sos
1093 Notes
1094 -----
1095 Changing the value of properties that are not part of the
1096 `ZerosPolesGain` system representation (such as the `A`, `B`, `C`, `D`
1097 state-space matrices) is very inefficient and may lead to numerical
1098 inaccuracies. It is better to convert to the specific system
1099 representation first. For example, call ``sys = sys.to_ss()`` before
1100 accessing/changing the A, B, C, D system matrices.
1102 Examples
1103 --------
1104 >>> from scipy import signal
1106 Transfer function: H(s) = 5(s - 1)(s - 2) / (s - 3)(s - 4)
1108 >>> signal.ZerosPolesGain([1, 2], [3, 4], 5)
1109 ZerosPolesGainContinuous(
1110 array([1, 2]),
1111 array([3, 4]),
1112 5,
1113 dt: None
1114 )
1116 """
1117 def to_discrete(self, dt, method='zoh', alpha=None):
1118 """
1119 Returns the discretized `ZerosPolesGain` system.
1121 Parameters: See `cont2discrete` for details.
1123 Returns
1124 -------
1125 sys: instance of `dlti` and `ZerosPolesGain`
1126 """
1127 return ZerosPolesGain(
1128 *cont2discrete((self.zeros, self.poles, self.gain),
1129 dt,
1130 method=method,
1131 alpha=alpha)[:-1],
1132 dt=dt)
1135class ZerosPolesGainDiscrete(ZerosPolesGain, dlti):
1136 r"""
1137 Discrete-time Linear Time Invariant system in zeros, poles, gain form.
1139 Represents the system as the discrete-time transfer function
1140 :math:`H(s)=k \prod_i (s - z[i]) / \prod_j (s - p[j])`, where :math:`k` is
1141 the `gain`, :math:`z` are the `zeros` and :math:`p` are the `poles`.
1142 Discrete-time `ZerosPolesGain` systems inherit additional functionality
1143 from the `dlti` class.
1145 Parameters
1146 ----------
1147 *system : arguments
1148 The `ZerosPolesGain` class can be instantiated with 1 or 3
1149 arguments. The following gives the number of input arguments and their
1150 interpretation:
1152 * 1: `dlti` system: (`StateSpace`, `TransferFunction` or
1153 `ZerosPolesGain`)
1154 * 3: array_like: (zeros, poles, gain)
1155 dt: float, optional
1156 Sampling time [s] of the discrete-time systems. Defaults to `True`
1157 (unspecified sampling time). Must be specified as a keyword argument,
1158 for example, ``dt=0.1``.
1160 See Also
1161 --------
1162 TransferFunction, StateSpace, dlti
1163 zpk2ss, zpk2tf, zpk2sos
1165 Notes
1166 -----
1167 Changing the value of properties that are not part of the
1168 `ZerosPolesGain` system representation (such as the `A`, `B`, `C`, `D`
1169 state-space matrices) is very inefficient and may lead to numerical
1170 inaccuracies. It is better to convert to the specific system
1171 representation first. For example, call ``sys = sys.to_ss()`` before
1172 accessing/changing the A, B, C, D system matrices.
1174 Examples
1175 --------
1176 >>> from scipy import signal
1178 Transfer function: H(s) = 5(s - 1)(s - 2) / (s - 3)(s - 4)
1180 >>> signal.ZerosPolesGain([1, 2], [3, 4], 5)
1181 ZerosPolesGainContinuous(
1182 array([1, 2]),
1183 array([3, 4]),
1184 5,
1185 dt: None
1186 )
1188 Transfer function: H(z) = 5(z - 1)(z - 2) / (z - 3)(z - 4)
1190 >>> signal.ZerosPolesGain([1, 2], [3, 4], 5, dt=0.1)
1191 ZerosPolesGainDiscrete(
1192 array([1, 2]),
1193 array([3, 4]),
1194 5,
1195 dt: 0.1
1196 )
1198 """
1199 pass
1202def _atleast_2d_or_none(arg):
1203 if arg is not None:
1204 return atleast_2d(arg)
1207class StateSpace(LinearTimeInvariant):
1208 r"""
1209 Linear Time Invariant system in state-space form.
1211 Represents the system as the continuous-time, first order differential
1212 equation :math:`\dot{x} = A x + B u` or the discrete-time difference
1213 equation :math:`x[k+1] = A x[k] + B u[k]`. `StateSpace` systems
1214 inherit additional functionality from the `lti`, respectively the `dlti`
1215 classes, depending on which system representation is used.
1217 Parameters
1218 ----------
1219 *system: arguments
1220 The `StateSpace` class can be instantiated with 1 or 3 arguments.
1221 The following gives the number of input arguments and their
1222 interpretation:
1224 * 1: `lti` or `dlti` system: (`StateSpace`, `TransferFunction` or
1225 `ZerosPolesGain`)
1226 * 4: array_like: (A, B, C, D)
1227 dt: float, optional
1228 Sampling time [s] of the discrete-time systems. Defaults to `None`
1229 (continuous-time). Must be specified as a keyword argument, for
1230 example, ``dt=0.1``.
1232 See Also
1233 --------
1234 TransferFunction, ZerosPolesGain, lti, dlti
1235 ss2zpk, ss2tf, zpk2sos
1237 Notes
1238 -----
1239 Changing the value of properties that are not part of the
1240 `StateSpace` system representation (such as `zeros` or `poles`) is very
1241 inefficient and may lead to numerical inaccuracies. It is better to
1242 convert to the specific system representation first. For example, call
1243 ``sys = sys.to_zpk()`` before accessing/changing the zeros, poles or gain.
1245 Examples
1246 --------
1247 >>> from scipy import signal
1249 >>> a = np.array([[0, 1], [0, 0]])
1250 >>> b = np.array([[0], [1]])
1251 >>> c = np.array([[1, 0]])
1252 >>> d = np.array([[0]])
1254 >>> sys = signal.StateSpace(a, b, c, d)
1255 >>> print(sys)
1256 StateSpaceContinuous(
1257 array([[0, 1],
1258 [0, 0]]),
1259 array([[0],
1260 [1]]),
1261 array([[1, 0]]),
1262 array([[0]]),
1263 dt: None
1264 )
1266 >>> sys.to_discrete(0.1)
1267 StateSpaceDiscrete(
1268 array([[1. , 0.1],
1269 [0. , 1. ]]),
1270 array([[0.005],
1271 [0.1 ]]),
1272 array([[1, 0]]),
1273 array([[0]]),
1274 dt: 0.1
1275 )
1277 >>> a = np.array([[1, 0.1], [0, 1]])
1278 >>> b = np.array([[0.005], [0.1]])
1280 >>> signal.StateSpace(a, b, c, d, dt=0.1)
1281 StateSpaceDiscrete(
1282 array([[1. , 0.1],
1283 [0. , 1. ]]),
1284 array([[0.005],
1285 [0.1 ]]),
1286 array([[1, 0]]),
1287 array([[0]]),
1288 dt: 0.1
1289 )
1291 """
1293 # Override NumPy binary operations and ufuncs
1294 __array_priority__ = 100.0
1295 __array_ufunc__ = None
1297 def __new__(cls, *system, **kwargs):
1298 """Create new StateSpace object and settle inheritance."""
1299 # Handle object conversion if input is an instance of `lti`
1300 if len(system) == 1 and isinstance(system[0], LinearTimeInvariant):
1301 return system[0].to_ss()
1303 # Choose whether to inherit from `lti` or from `dlti`
1304 if cls is StateSpace:
1305 if kwargs.get('dt') is None:
1306 return StateSpaceContinuous.__new__(StateSpaceContinuous,
1307 *system, **kwargs)
1308 else:
1309 return StateSpaceDiscrete.__new__(StateSpaceDiscrete,
1310 *system, **kwargs)
1312 # No special conversion needed
1313 return super(StateSpace, cls).__new__(cls)
1315 def __init__(self, *system, **kwargs):
1316 """Initialize the state space lti/dlti system."""
1317 # Conversion of lti instances is handled in __new__
1318 if isinstance(system[0], LinearTimeInvariant):
1319 return
1321 # Remove system arguments, not needed by parents anymore
1322 super(StateSpace, self).__init__(**kwargs)
1324 self._A = None
1325 self._B = None
1326 self._C = None
1327 self._D = None
1329 self.A, self.B, self.C, self.D = abcd_normalize(*system)
1331 def __repr__(self):
1332 """Return representation of the `StateSpace` system."""
1333 return '{0}(\n{1},\n{2},\n{3},\n{4},\ndt: {5}\n)'.format(
1334 self.__class__.__name__,
1335 repr(self.A),
1336 repr(self.B),
1337 repr(self.C),
1338 repr(self.D),
1339 repr(self.dt),
1340 )
1342 def _check_binop_other(self, other):
1343 return isinstance(other, (StateSpace, np.ndarray, float, complex,
1344 np.number, int))
1346 def __mul__(self, other):
1347 """
1348 Post-multiply another system or a scalar
1350 Handles multiplication of systems in the sense of a frequency domain
1351 multiplication. That means, given two systems E1(s) and E2(s), their
1352 multiplication, H(s) = E1(s) * E2(s), means that applying H(s) to U(s)
1353 is equivalent to first applying E2(s), and then E1(s).
1355 Notes
1356 -----
1357 For SISO systems the order of system application does not matter.
1358 However, for MIMO systems, where the two systems are matrices, the
1359 order above ensures standard Matrix multiplication rules apply.
1360 """
1361 if not self._check_binop_other(other):
1362 return NotImplemented
1364 if isinstance(other, StateSpace):
1365 # Disallow mix of discrete and continuous systems.
1366 if type(other) is not type(self):
1367 return NotImplemented
1369 if self.dt != other.dt:
1370 raise TypeError('Cannot multiply systems with different `dt`.')
1372 n1 = self.A.shape[0]
1373 n2 = other.A.shape[0]
1375 # Interconnection of systems
1376 # x1' = A1 x1 + B1 u1
1377 # y1 = C1 x1 + D1 u1
1378 # x2' = A2 x2 + B2 y1
1379 # y2 = C2 x2 + D2 y1
1380 #
1381 # Plugging in with u1 = y2 yields
1382 # [x1'] [A1 B1*C2 ] [x1] [B1*D2]
1383 # [x2'] = [0 A2 ] [x2] + [B2 ] u2
1384 # [x1]
1385 # y2 = [C1 D1*C2] [x2] + D1*D2 u2
1386 a = np.vstack((np.hstack((self.A, np.dot(self.B, other.C))),
1387 np.hstack((zeros((n2, n1)), other.A))))
1388 b = np.vstack((np.dot(self.B, other.D), other.B))
1389 c = np.hstack((self.C, np.dot(self.D, other.C)))
1390 d = np.dot(self.D, other.D)
1391 else:
1392 # Assume that other is a scalar / matrix
1393 # For post multiplication the input gets scaled
1394 a = self.A
1395 b = np.dot(self.B, other)
1396 c = self.C
1397 d = np.dot(self.D, other)
1399 common_dtype = np.find_common_type((a.dtype, b.dtype, c.dtype, d.dtype), ())
1400 return StateSpace(np.asarray(a, dtype=common_dtype),
1401 np.asarray(b, dtype=common_dtype),
1402 np.asarray(c, dtype=common_dtype),
1403 np.asarray(d, dtype=common_dtype),
1404 **self._dt_dict)
1406 def __rmul__(self, other):
1407 """Pre-multiply a scalar or matrix (but not StateSpace)"""
1408 if not self._check_binop_other(other) or isinstance(other, StateSpace):
1409 return NotImplemented
1411 # For pre-multiplication only the output gets scaled
1412 a = self.A
1413 b = self.B
1414 c = np.dot(other, self.C)
1415 d = np.dot(other, self.D)
1417 common_dtype = np.find_common_type((a.dtype, b.dtype, c.dtype, d.dtype), ())
1418 return StateSpace(np.asarray(a, dtype=common_dtype),
1419 np.asarray(b, dtype=common_dtype),
1420 np.asarray(c, dtype=common_dtype),
1421 np.asarray(d, dtype=common_dtype),
1422 **self._dt_dict)
1424 def __neg__(self):
1425 """Negate the system (equivalent to pre-multiplying by -1)."""
1426 return StateSpace(self.A, self.B, -self.C, -self.D, **self._dt_dict)
1428 def __add__(self, other):
1429 """
1430 Adds two systems in the sense of frequency domain addition.
1431 """
1432 if not self._check_binop_other(other):
1433 return NotImplemented
1435 if isinstance(other, StateSpace):
1436 # Disallow mix of discrete and continuous systems.
1437 if type(other) is not type(self):
1438 raise TypeError('Cannot add {} and {}'.format(type(self),
1439 type(other)))
1441 if self.dt != other.dt:
1442 raise TypeError('Cannot add systems with different `dt`.')
1443 # Interconnection of systems
1444 # x1' = A1 x1 + B1 u
1445 # y1 = C1 x1 + D1 u
1446 # x2' = A2 x2 + B2 u
1447 # y2 = C2 x2 + D2 u
1448 # y = y1 + y2
1449 #
1450 # Plugging in yields
1451 # [x1'] [A1 0 ] [x1] [B1]
1452 # [x2'] = [0 A2] [x2] + [B2] u
1453 # [x1]
1454 # y = [C1 C2] [x2] + [D1 + D2] u
1455 a = linalg.block_diag(self.A, other.A)
1456 b = np.vstack((self.B, other.B))
1457 c = np.hstack((self.C, other.C))
1458 d = self.D + other.D
1459 else:
1460 other = np.atleast_2d(other)
1461 if self.D.shape == other.shape:
1462 # A scalar/matrix is really just a static system (A=0, B=0, C=0)
1463 a = self.A
1464 b = self.B
1465 c = self.C
1466 d = self.D + other
1467 else:
1468 raise ValueError("Cannot add systems with incompatible dimensions")
1470 common_dtype = np.find_common_type((a.dtype, b.dtype, c.dtype, d.dtype), ())
1471 return StateSpace(np.asarray(a, dtype=common_dtype),
1472 np.asarray(b, dtype=common_dtype),
1473 np.asarray(c, dtype=common_dtype),
1474 np.asarray(d, dtype=common_dtype),
1475 **self._dt_dict)
1477 def __sub__(self, other):
1478 if not self._check_binop_other(other):
1479 return NotImplemented
1481 return self.__add__(-other)
1483 def __radd__(self, other):
1484 if not self._check_binop_other(other):
1485 return NotImplemented
1487 return self.__add__(other)
1489 def __rsub__(self, other):
1490 if not self._check_binop_other(other):
1491 return NotImplemented
1493 return (-self).__add__(other)
1495 def __truediv__(self, other):
1496 """
1497 Divide by a scalar
1498 """
1499 # Division by non-StateSpace scalars
1500 if not self._check_binop_other(other) or isinstance(other, StateSpace):
1501 return NotImplemented
1503 if isinstance(other, np.ndarray) and other.ndim > 0:
1504 # It's ambiguous what this means, so disallow it
1505 raise ValueError("Cannot divide StateSpace by non-scalar numpy arrays")
1507 return self.__mul__(1/other)
1509 @property
1510 def A(self):
1511 """State matrix of the `StateSpace` system."""
1512 return self._A
1514 @A.setter
1515 def A(self, A):
1516 self._A = _atleast_2d_or_none(A)
1518 @property
1519 def B(self):
1520 """Input matrix of the `StateSpace` system."""
1521 return self._B
1523 @B.setter
1524 def B(self, B):
1525 self._B = _atleast_2d_or_none(B)
1526 self.inputs = self.B.shape[-1]
1528 @property
1529 def C(self):
1530 """Output matrix of the `StateSpace` system."""
1531 return self._C
1533 @C.setter
1534 def C(self, C):
1535 self._C = _atleast_2d_or_none(C)
1536 self.outputs = self.C.shape[0]
1538 @property
1539 def D(self):
1540 """Feedthrough matrix of the `StateSpace` system."""
1541 return self._D
1543 @D.setter
1544 def D(self, D):
1545 self._D = _atleast_2d_or_none(D)
1547 def _copy(self, system):
1548 """
1549 Copy the parameters of another `StateSpace` system.
1551 Parameters
1552 ----------
1553 system : instance of `StateSpace`
1554 The state-space system that is to be copied
1556 """
1557 self.A = system.A
1558 self.B = system.B
1559 self.C = system.C
1560 self.D = system.D
1562 def to_tf(self, **kwargs):
1563 """
1564 Convert system representation to `TransferFunction`.
1566 Parameters
1567 ----------
1568 kwargs : dict, optional
1569 Additional keywords passed to `ss2zpk`
1571 Returns
1572 -------
1573 sys : instance of `TransferFunction`
1574 Transfer function of the current system
1576 """
1577 return TransferFunction(*ss2tf(self._A, self._B, self._C, self._D,
1578 **kwargs), **self._dt_dict)
1580 def to_zpk(self, **kwargs):
1581 """
1582 Convert system representation to `ZerosPolesGain`.
1584 Parameters
1585 ----------
1586 kwargs : dict, optional
1587 Additional keywords passed to `ss2zpk`
1589 Returns
1590 -------
1591 sys : instance of `ZerosPolesGain`
1592 Zeros, poles, gain representation of the current system
1594 """
1595 return ZerosPolesGain(*ss2zpk(self._A, self._B, self._C, self._D,
1596 **kwargs), **self._dt_dict)
1598 def to_ss(self):
1599 """
1600 Return a copy of the current `StateSpace` system.
1602 Returns
1603 -------
1604 sys : instance of `StateSpace`
1605 The current system (copy)
1607 """
1608 return copy.deepcopy(self)
1611class StateSpaceContinuous(StateSpace, lti):
1612 r"""
1613 Continuous-time Linear Time Invariant system in state-space form.
1615 Represents the system as the continuous-time, first order differential
1616 equation :math:`\dot{x} = A x + B u`.
1617 Continuous-time `StateSpace` systems inherit additional functionality
1618 from the `lti` class.
1620 Parameters
1621 ----------
1622 *system: arguments
1623 The `StateSpace` class can be instantiated with 1 or 3 arguments.
1624 The following gives the number of input arguments and their
1625 interpretation:
1627 * 1: `lti` system: (`StateSpace`, `TransferFunction` or
1628 `ZerosPolesGain`)
1629 * 4: array_like: (A, B, C, D)
1631 See Also
1632 --------
1633 TransferFunction, ZerosPolesGain, lti
1634 ss2zpk, ss2tf, zpk2sos
1636 Notes
1637 -----
1638 Changing the value of properties that are not part of the
1639 `StateSpace` system representation (such as `zeros` or `poles`) is very
1640 inefficient and may lead to numerical inaccuracies. It is better to
1641 convert to the specific system representation first. For example, call
1642 ``sys = sys.to_zpk()`` before accessing/changing the zeros, poles or gain.
1644 Examples
1645 --------
1646 >>> from scipy import signal
1648 >>> a = np.array([[0, 1], [0, 0]])
1649 >>> b = np.array([[0], [1]])
1650 >>> c = np.array([[1, 0]])
1651 >>> d = np.array([[0]])
1653 >>> sys = signal.StateSpace(a, b, c, d)
1654 >>> print(sys)
1655 StateSpaceContinuous(
1656 array([[0, 1],
1657 [0, 0]]),
1658 array([[0],
1659 [1]]),
1660 array([[1, 0]]),
1661 array([[0]]),
1662 dt: None
1663 )
1665 """
1666 def to_discrete(self, dt, method='zoh', alpha=None):
1667 """
1668 Returns the discretized `StateSpace` system.
1670 Parameters: See `cont2discrete` for details.
1672 Returns
1673 -------
1674 sys: instance of `dlti` and `StateSpace`
1675 """
1676 return StateSpace(*cont2discrete((self.A, self.B, self.C, self.D),
1677 dt,
1678 method=method,
1679 alpha=alpha)[:-1],
1680 dt=dt)
1683class StateSpaceDiscrete(StateSpace, dlti):
1684 r"""
1685 Discrete-time Linear Time Invariant system in state-space form.
1687 Represents the system as the discrete-time difference equation
1688 :math:`x[k+1] = A x[k] + B u[k]`.
1689 `StateSpace` systems inherit additional functionality from the `dlti`
1690 class.
1692 Parameters
1693 ----------
1694 *system: arguments
1695 The `StateSpace` class can be instantiated with 1 or 3 arguments.
1696 The following gives the number of input arguments and their
1697 interpretation:
1699 * 1: `dlti` system: (`StateSpace`, `TransferFunction` or
1700 `ZerosPolesGain`)
1701 * 4: array_like: (A, B, C, D)
1702 dt: float, optional
1703 Sampling time [s] of the discrete-time systems. Defaults to `True`
1704 (unspecified sampling time). Must be specified as a keyword argument,
1705 for example, ``dt=0.1``.
1707 See Also
1708 --------
1709 TransferFunction, ZerosPolesGain, dlti
1710 ss2zpk, ss2tf, zpk2sos
1712 Notes
1713 -----
1714 Changing the value of properties that are not part of the
1715 `StateSpace` system representation (such as `zeros` or `poles`) is very
1716 inefficient and may lead to numerical inaccuracies. It is better to
1717 convert to the specific system representation first. For example, call
1718 ``sys = sys.to_zpk()`` before accessing/changing the zeros, poles or gain.
1720 Examples
1721 --------
1722 >>> from scipy import signal
1724 >>> a = np.array([[1, 0.1], [0, 1]])
1725 >>> b = np.array([[0.005], [0.1]])
1726 >>> c = np.array([[1, 0]])
1727 >>> d = np.array([[0]])
1729 >>> signal.StateSpace(a, b, c, d, dt=0.1)
1730 StateSpaceDiscrete(
1731 array([[ 1. , 0.1],
1732 [ 0. , 1. ]]),
1733 array([[ 0.005],
1734 [ 0.1 ]]),
1735 array([[1, 0]]),
1736 array([[0]]),
1737 dt: 0.1
1738 )
1740 """
1741 pass
1744def lsim2(system, U=None, T=None, X0=None, **kwargs):
1745 """
1746 Simulate output of a continuous-time linear system, by using
1747 the ODE solver `scipy.integrate.odeint`.
1749 Parameters
1750 ----------
1751 system : an instance of the `lti` class or a tuple describing the system.
1752 The following gives the number of elements in the tuple and
1753 the interpretation:
1755 * 1: (instance of `lti`)
1756 * 2: (num, den)
1757 * 3: (zeros, poles, gain)
1758 * 4: (A, B, C, D)
1760 U : array_like (1D or 2D), optional
1761 An input array describing the input at each time T. Linear
1762 interpolation is used between given times. If there are
1763 multiple inputs, then each column of the rank-2 array
1764 represents an input. If U is not given, the input is assumed
1765 to be zero.
1766 T : array_like (1D or 2D), optional
1767 The time steps at which the input is defined and at which the
1768 output is desired. The default is 101 evenly spaced points on
1769 the interval [0,10.0].
1770 X0 : array_like (1D), optional
1771 The initial condition of the state vector. If `X0` is not
1772 given, the initial conditions are assumed to be 0.
1773 kwargs : dict
1774 Additional keyword arguments are passed on to the function
1775 `odeint`. See the notes below for more details.
1777 Returns
1778 -------
1779 T : 1D ndarray
1780 The time values for the output.
1781 yout : ndarray
1782 The response of the system.
1783 xout : ndarray
1784 The time-evolution of the state-vector.
1786 Notes
1787 -----
1788 This function uses `scipy.integrate.odeint` to solve the
1789 system's differential equations. Additional keyword arguments
1790 given to `lsim2` are passed on to `odeint`. See the documentation
1791 for `scipy.integrate.odeint` for the full list of arguments.
1793 If (num, den) is passed in for ``system``, coefficients for both the
1794 numerator and denominator should be specified in descending exponent
1795 order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
1797 See Also
1798 --------
1799 lsim
1801 Examples
1802 --------
1803 We'll use `lsim2` to simulate an analog Bessel filter applied to
1804 a signal.
1806 >>> from scipy.signal import bessel, lsim2
1807 >>> import matplotlib.pyplot as plt
1809 Create a low-pass Bessel filter with a cutoff of 12 Hz.
1811 >>> b, a = bessel(N=5, Wn=2*np.pi*12, btype='lowpass', analog=True)
1813 Generate data to which the filter is applied.
1815 >>> t = np.linspace(0, 1.25, 500, endpoint=False)
1817 The input signal is the sum of three sinusoidal curves, with
1818 frequencies 4 Hz, 40 Hz, and 80 Hz. The filter should mostly
1819 eliminate the 40 Hz and 80 Hz components, leaving just the 4 Hz signal.
1821 >>> u = (np.cos(2*np.pi*4*t) + 0.6*np.sin(2*np.pi*40*t) +
1822 ... 0.5*np.cos(2*np.pi*80*t))
1824 Simulate the filter with `lsim2`.
1826 >>> tout, yout, xout = lsim2((b, a), U=u, T=t)
1828 Plot the result.
1830 >>> plt.plot(t, u, 'r', alpha=0.5, linewidth=1, label='input')
1831 >>> plt.plot(tout, yout, 'k', linewidth=1.5, label='output')
1832 >>> plt.legend(loc='best', shadow=True, framealpha=1)
1833 >>> plt.grid(alpha=0.3)
1834 >>> plt.xlabel('t')
1835 >>> plt.show()
1837 In a second example, we simulate a double integrator ``y'' = u``, with
1838 a constant input ``u = 1``. We'll use the state space representation
1839 of the integrator.
1841 >>> from scipy.signal import lti
1842 >>> A = np.array([[0, 1], [0, 0]])
1843 >>> B = np.array([[0], [1]])
1844 >>> C = np.array([[1, 0]])
1845 >>> D = 0
1846 >>> system = lti(A, B, C, D)
1848 `t` and `u` define the time and input signal for the system to
1849 be simulated.
1851 >>> t = np.linspace(0, 5, num=50)
1852 >>> u = np.ones_like(t)
1854 Compute the simulation, and then plot `y`. As expected, the plot shows
1855 the curve ``y = 0.5*t**2``.
1857 >>> tout, y, x = lsim2(system, u, t)
1858 >>> plt.plot(t, y)
1859 >>> plt.grid(alpha=0.3)
1860 >>> plt.xlabel('t')
1861 >>> plt.show()
1863 """
1864 if isinstance(system, lti):
1865 sys = system._as_ss()
1866 elif isinstance(system, dlti):
1867 raise AttributeError('lsim2 can only be used with continuous-time '
1868 'systems.')
1869 else:
1870 sys = lti(*system)._as_ss()
1872 if X0 is None:
1873 X0 = zeros(sys.B.shape[0], sys.A.dtype)
1875 if T is None:
1876 # XXX T should really be a required argument, but U was
1877 # changed from a required positional argument to a keyword,
1878 # and T is after U in the argument list. So we either: change
1879 # the API and move T in front of U; check here for T being
1880 # None and raise an exception; or assign a default value to T
1881 # here. This code implements the latter.
1882 T = linspace(0, 10.0, 101)
1884 T = atleast_1d(T)
1885 if len(T.shape) != 1:
1886 raise ValueError("T must be a rank-1 array.")
1888 if U is not None:
1889 U = atleast_1d(U)
1890 if len(U.shape) == 1:
1891 U = U.reshape(-1, 1)
1892 sU = U.shape
1893 if sU[0] != len(T):
1894 raise ValueError("U must have the same number of rows "
1895 "as elements in T.")
1897 if sU[1] != sys.inputs:
1898 raise ValueError("The number of inputs in U (%d) is not "
1899 "compatible with the number of system "
1900 "inputs (%d)" % (sU[1], sys.inputs))
1901 # Create a callable that uses linear interpolation to
1902 # calculate the input at any time.
1903 ufunc = interpolate.interp1d(T, U, kind='linear',
1904 axis=0, bounds_error=False)
1906 def fprime(x, t, sys, ufunc):
1907 """The vector field of the linear system."""
1908 return dot(sys.A, x) + squeeze(dot(sys.B, nan_to_num(ufunc([t]))))
1909 xout = integrate.odeint(fprime, X0, T, args=(sys, ufunc), **kwargs)
1910 yout = dot(sys.C, transpose(xout)) + dot(sys.D, transpose(U))
1911 else:
1912 def fprime(x, t, sys):
1913 """The vector field of the linear system."""
1914 return dot(sys.A, x)
1915 xout = integrate.odeint(fprime, X0, T, args=(sys,), **kwargs)
1916 yout = dot(sys.C, transpose(xout))
1918 return T, squeeze(transpose(yout)), xout
1921def _cast_to_array_dtype(in1, in2):
1922 """Cast array to dtype of other array, while avoiding ComplexWarning.
1924 Those can be raised when casting complex to real.
1925 """
1926 if numpy.issubdtype(in2.dtype, numpy.float):
1927 # dtype to cast to is not complex, so use .real
1928 in1 = in1.real.astype(in2.dtype)
1929 else:
1930 in1 = in1.astype(in2.dtype)
1932 return in1
1935def lsim(system, U, T, X0=None, interp=True):
1936 """
1937 Simulate output of a continuous-time linear system.
1939 Parameters
1940 ----------
1941 system : an instance of the LTI class or a tuple describing the system.
1942 The following gives the number of elements in the tuple and
1943 the interpretation:
1945 * 1: (instance of `lti`)
1946 * 2: (num, den)
1947 * 3: (zeros, poles, gain)
1948 * 4: (A, B, C, D)
1950 U : array_like
1951 An input array describing the input at each time `T`
1952 (interpolation is assumed between given times). If there are
1953 multiple inputs, then each column of the rank-2 array
1954 represents an input. If U = 0 or None, a zero input is used.
1955 T : array_like
1956 The time steps at which the input is defined and at which the
1957 output is desired. Must be nonnegative, increasing, and equally spaced.
1958 X0 : array_like, optional
1959 The initial conditions on the state vector (zero by default).
1960 interp : bool, optional
1961 Whether to use linear (True, the default) or zero-order-hold (False)
1962 interpolation for the input array.
1964 Returns
1965 -------
1966 T : 1D ndarray
1967 Time values for the output.
1968 yout : 1D ndarray
1969 System response.
1970 xout : ndarray
1971 Time evolution of the state vector.
1973 Notes
1974 -----
1975 If (num, den) is passed in for ``system``, coefficients for both the
1976 numerator and denominator should be specified in descending exponent
1977 order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
1979 Examples
1980 --------
1981 We'll use `lsim` to simulate an analog Bessel filter applied to
1982 a signal.
1984 >>> from scipy.signal import bessel, lsim
1985 >>> import matplotlib.pyplot as plt
1987 Create a low-pass Bessel filter with a cutoff of 12 Hz.
1989 >>> b, a = bessel(N=5, Wn=2*np.pi*12, btype='lowpass', analog=True)
1991 Generate data to which the filter is applied.
1993 >>> t = np.linspace(0, 1.25, 500, endpoint=False)
1995 The input signal is the sum of three sinusoidal curves, with
1996 frequencies 4 Hz, 40 Hz, and 80 Hz. The filter should mostly
1997 eliminate the 40 Hz and 80 Hz components, leaving just the 4 Hz signal.
1999 >>> u = (np.cos(2*np.pi*4*t) + 0.6*np.sin(2*np.pi*40*t) +
2000 ... 0.5*np.cos(2*np.pi*80*t))
2002 Simulate the filter with `lsim`.
2004 >>> tout, yout, xout = lsim((b, a), U=u, T=t)
2006 Plot the result.
2008 >>> plt.plot(t, u, 'r', alpha=0.5, linewidth=1, label='input')
2009 >>> plt.plot(tout, yout, 'k', linewidth=1.5, label='output')
2010 >>> plt.legend(loc='best', shadow=True, framealpha=1)
2011 >>> plt.grid(alpha=0.3)
2012 >>> plt.xlabel('t')
2013 >>> plt.show()
2015 In a second example, we simulate a double integrator ``y'' = u``, with
2016 a constant input ``u = 1``. We'll use the state space representation
2017 of the integrator.
2019 >>> from scipy.signal import lti
2020 >>> A = np.array([[0.0, 1.0], [0.0, 0.0]])
2021 >>> B = np.array([[0.0], [1.0]])
2022 >>> C = np.array([[1.0, 0.0]])
2023 >>> D = 0.0
2024 >>> system = lti(A, B, C, D)
2026 `t` and `u` define the time and input signal for the system to
2027 be simulated.
2029 >>> t = np.linspace(0, 5, num=50)
2030 >>> u = np.ones_like(t)
2032 Compute the simulation, and then plot `y`. As expected, the plot shows
2033 the curve ``y = 0.5*t**2``.
2035 >>> tout, y, x = lsim(system, u, t)
2036 >>> plt.plot(t, y)
2037 >>> plt.grid(alpha=0.3)
2038 >>> plt.xlabel('t')
2039 >>> plt.show()
2041 """
2042 if isinstance(system, lti):
2043 sys = system._as_ss()
2044 elif isinstance(system, dlti):
2045 raise AttributeError('lsim can only be used with continuous-time '
2046 'systems.')
2047 else:
2048 sys = lti(*system)._as_ss()
2049 T = atleast_1d(T)
2050 if len(T.shape) != 1:
2051 raise ValueError("T must be a rank-1 array.")
2053 A, B, C, D = map(np.asarray, (sys.A, sys.B, sys.C, sys.D))
2054 n_states = A.shape[0]
2055 n_inputs = B.shape[1]
2057 n_steps = T.size
2058 if X0 is None:
2059 X0 = zeros(n_states, sys.A.dtype)
2060 xout = zeros((n_steps, n_states), sys.A.dtype)
2062 if T[0] == 0:
2063 xout[0] = X0
2064 elif T[0] > 0:
2065 # step forward to initial time, with zero input
2066 xout[0] = dot(X0, linalg.expm(transpose(A) * T[0]))
2067 else:
2068 raise ValueError("Initial time must be nonnegative")
2070 no_input = (U is None or
2071 (isinstance(U, (int, float)) and U == 0.) or
2072 not np.any(U))
2074 if n_steps == 1:
2075 yout = squeeze(dot(xout, transpose(C)))
2076 if not no_input:
2077 yout += squeeze(dot(U, transpose(D)))
2078 return T, squeeze(yout), squeeze(xout)
2080 dt = T[1] - T[0]
2081 if not np.allclose((T[1:] - T[:-1]) / dt, 1.0):
2082 warnings.warn("Non-uniform timesteps are deprecated. Results may be "
2083 "slow and/or inaccurate.", DeprecationWarning)
2084 return lsim2(system, U, T, X0)
2086 if no_input:
2087 # Zero input: just use matrix exponential
2088 # take transpose because state is a row vector
2089 expAT_dt = linalg.expm(transpose(A) * dt)
2090 for i in range(1, n_steps):
2091 xout[i] = dot(xout[i-1], expAT_dt)
2092 yout = squeeze(dot(xout, transpose(C)))
2093 return T, squeeze(yout), squeeze(xout)
2095 # Nonzero input
2096 U = atleast_1d(U)
2097 if U.ndim == 1:
2098 U = U[:, np.newaxis]
2100 if U.shape[0] != n_steps:
2101 raise ValueError("U must have the same number of rows "
2102 "as elements in T.")
2104 if U.shape[1] != n_inputs:
2105 raise ValueError("System does not define that many inputs.")
2107 if not interp:
2108 # Zero-order hold
2109 # Algorithm: to integrate from time 0 to time dt, we solve
2110 # xdot = A x + B u, x(0) = x0
2111 # udot = 0, u(0) = u0.
2112 #
2113 # Solution is
2114 # [ x(dt) ] [ A*dt B*dt ] [ x0 ]
2115 # [ u(dt) ] = exp [ 0 0 ] [ u0 ]
2116 M = np.vstack([np.hstack([A * dt, B * dt]),
2117 np.zeros((n_inputs, n_states + n_inputs))])
2118 # transpose everything because the state and input are row vectors
2119 expMT = linalg.expm(transpose(M))
2120 Ad = expMT[:n_states, :n_states]
2121 Bd = expMT[n_states:, :n_states]
2122 for i in range(1, n_steps):
2123 xout[i] = dot(xout[i-1], Ad) + dot(U[i-1], Bd)
2124 else:
2125 # Linear interpolation between steps
2126 # Algorithm: to integrate from time 0 to time dt, with linear
2127 # interpolation between inputs u(0) = u0 and u(dt) = u1, we solve
2128 # xdot = A x + B u, x(0) = x0
2129 # udot = (u1 - u0) / dt, u(0) = u0.
2130 #
2131 # Solution is
2132 # [ x(dt) ] [ A*dt B*dt 0 ] [ x0 ]
2133 # [ u(dt) ] = exp [ 0 0 I ] [ u0 ]
2134 # [u1 - u0] [ 0 0 0 ] [u1 - u0]
2135 M = np.vstack([np.hstack([A * dt, B * dt,
2136 np.zeros((n_states, n_inputs))]),
2137 np.hstack([np.zeros((n_inputs, n_states + n_inputs)),
2138 np.identity(n_inputs)]),
2139 np.zeros((n_inputs, n_states + 2 * n_inputs))])
2140 expMT = linalg.expm(transpose(M))
2141 Ad = expMT[:n_states, :n_states]
2142 Bd1 = expMT[n_states+n_inputs:, :n_states]
2143 Bd0 = expMT[n_states:n_states + n_inputs, :n_states] - Bd1
2144 for i in range(1, n_steps):
2145 xout[i] = (dot(xout[i-1], Ad) + dot(U[i-1], Bd0) + dot(U[i], Bd1))
2147 yout = (squeeze(dot(xout, transpose(C))) + squeeze(dot(U, transpose(D))))
2148 return T, squeeze(yout), squeeze(xout)
2151def _default_response_times(A, n):
2152 """Compute a reasonable set of time samples for the response time.
2154 This function is used by `impulse`, `impulse2`, `step` and `step2`
2155 to compute the response time when the `T` argument to the function
2156 is None.
2158 Parameters
2159 ----------
2160 A : array_like
2161 The system matrix, which is square.
2162 n : int
2163 The number of time samples to generate.
2165 Returns
2166 -------
2167 t : ndarray
2168 The 1-D array of length `n` of time samples at which the response
2169 is to be computed.
2170 """
2171 # Create a reasonable time interval.
2172 # TODO: This could use some more work.
2173 # For example, what is expected when the system is unstable?
2174 vals = linalg.eigvals(A)
2175 r = min(abs(real(vals)))
2176 if r == 0.0:
2177 r = 1.0
2178 tc = 1.0 / r
2179 t = linspace(0.0, 7 * tc, n)
2180 return t
2183def impulse(system, X0=None, T=None, N=None):
2184 """Impulse response of continuous-time system.
2186 Parameters
2187 ----------
2188 system : an instance of the LTI class or a tuple of array_like
2189 describing the system.
2190 The following gives the number of elements in the tuple and
2191 the interpretation:
2193 * 1 (instance of `lti`)
2194 * 2 (num, den)
2195 * 3 (zeros, poles, gain)
2196 * 4 (A, B, C, D)
2198 X0 : array_like, optional
2199 Initial state-vector. Defaults to zero.
2200 T : array_like, optional
2201 Time points. Computed if not given.
2202 N : int, optional
2203 The number of time points to compute (if `T` is not given).
2205 Returns
2206 -------
2207 T : ndarray
2208 A 1-D array of time points.
2209 yout : ndarray
2210 A 1-D array containing the impulse response of the system (except for
2211 singularities at zero).
2213 Notes
2214 -----
2215 If (num, den) is passed in for ``system``, coefficients for both the
2216 numerator and denominator should be specified in descending exponent
2217 order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
2219 Examples
2220 --------
2221 Compute the impulse response of a second order system with a repeated
2222 root: ``x''(t) + 2*x'(t) + x(t) = u(t)``
2224 >>> from scipy import signal
2225 >>> system = ([1.0], [1.0, 2.0, 1.0])
2226 >>> t, y = signal.impulse(system)
2227 >>> import matplotlib.pyplot as plt
2228 >>> plt.plot(t, y)
2230 """
2231 if isinstance(system, lti):
2232 sys = system._as_ss()
2233 elif isinstance(system, dlti):
2234 raise AttributeError('impulse can only be used with continuous-time '
2235 'systems.')
2236 else:
2237 sys = lti(*system)._as_ss()
2238 if X0 is None:
2239 X = squeeze(sys.B)
2240 else:
2241 X = squeeze(sys.B + X0)
2242 if N is None:
2243 N = 100
2244 if T is None:
2245 T = _default_response_times(sys.A, N)
2246 else:
2247 T = asarray(T)
2249 _, h, _ = lsim(sys, 0., T, X, interp=False)
2250 return T, h
2253def impulse2(system, X0=None, T=None, N=None, **kwargs):
2254 """
2255 Impulse response of a single-input, continuous-time linear system.
2257 Parameters
2258 ----------
2259 system : an instance of the LTI class or a tuple of array_like
2260 describing the system.
2261 The following gives the number of elements in the tuple and
2262 the interpretation:
2264 * 1 (instance of `lti`)
2265 * 2 (num, den)
2266 * 3 (zeros, poles, gain)
2267 * 4 (A, B, C, D)
2269 X0 : 1-D array_like, optional
2270 The initial condition of the state vector. Default: 0 (the
2271 zero vector).
2272 T : 1-D array_like, optional
2273 The time steps at which the input is defined and at which the
2274 output is desired. If `T` is not given, the function will
2275 generate a set of time samples automatically.
2276 N : int, optional
2277 Number of time points to compute. Default: 100.
2278 kwargs : various types
2279 Additional keyword arguments are passed on to the function
2280 `scipy.signal.lsim2`, which in turn passes them on to
2281 `scipy.integrate.odeint`; see the latter's documentation for
2282 information about these arguments.
2284 Returns
2285 -------
2286 T : ndarray
2287 The time values for the output.
2288 yout : ndarray
2289 The output response of the system.
2291 See Also
2292 --------
2293 impulse, lsim2, scipy.integrate.odeint
2295 Notes
2296 -----
2297 The solution is generated by calling `scipy.signal.lsim2`, which uses
2298 the differential equation solver `scipy.integrate.odeint`.
2300 If (num, den) is passed in for ``system``, coefficients for both the
2301 numerator and denominator should be specified in descending exponent
2302 order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
2304 .. versionadded:: 0.8.0
2306 Examples
2307 --------
2308 Compute the impulse response of a second order system with a repeated
2309 root: ``x''(t) + 2*x'(t) + x(t) = u(t)``
2311 >>> from scipy import signal
2312 >>> system = ([1.0], [1.0, 2.0, 1.0])
2313 >>> t, y = signal.impulse2(system)
2314 >>> import matplotlib.pyplot as plt
2315 >>> plt.plot(t, y)
2317 """
2318 if isinstance(system, lti):
2319 sys = system._as_ss()
2320 elif isinstance(system, dlti):
2321 raise AttributeError('impulse2 can only be used with continuous-time '
2322 'systems.')
2323 else:
2324 sys = lti(*system)._as_ss()
2325 B = sys.B
2326 if B.shape[-1] != 1:
2327 raise ValueError("impulse2() requires a single-input system.")
2328 B = B.squeeze()
2329 if X0 is None:
2330 X0 = zeros_like(B)
2331 if N is None:
2332 N = 100
2333 if T is None:
2334 T = _default_response_times(sys.A, N)
2336 # Move the impulse in the input to the initial conditions, and then
2337 # solve using lsim2().
2338 ic = B + X0
2339 Tr, Yr, Xr = lsim2(sys, T=T, X0=ic, **kwargs)
2340 return Tr, Yr
2343def step(system, X0=None, T=None, N=None):
2344 """Step response of continuous-time system.
2346 Parameters
2347 ----------
2348 system : an instance of the LTI class or a tuple of array_like
2349 describing the system.
2350 The following gives the number of elements in the tuple and
2351 the interpretation:
2353 * 1 (instance of `lti`)
2354 * 2 (num, den)
2355 * 3 (zeros, poles, gain)
2356 * 4 (A, B, C, D)
2358 X0 : array_like, optional
2359 Initial state-vector (default is zero).
2360 T : array_like, optional
2361 Time points (computed if not given).
2362 N : int, optional
2363 Number of time points to compute if `T` is not given.
2365 Returns
2366 -------
2367 T : 1D ndarray
2368 Output time points.
2369 yout : 1D ndarray
2370 Step response of system.
2372 See also
2373 --------
2374 scipy.signal.step2
2376 Notes
2377 -----
2378 If (num, den) is passed in for ``system``, coefficients for both the
2379 numerator and denominator should be specified in descending exponent
2380 order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
2382 Examples
2383 --------
2384 >>> from scipy import signal
2385 >>> import matplotlib.pyplot as plt
2386 >>> lti = signal.lti([1.0], [1.0, 1.0])
2387 >>> t, y = signal.step(lti)
2388 >>> plt.plot(t, y)
2389 >>> plt.xlabel('Time [s]')
2390 >>> plt.ylabel('Amplitude')
2391 >>> plt.title('Step response for 1. Order Lowpass')
2392 >>> plt.grid()
2394 """
2395 if isinstance(system, lti):
2396 sys = system._as_ss()
2397 elif isinstance(system, dlti):
2398 raise AttributeError('step can only be used with continuous-time '
2399 'systems.')
2400 else:
2401 sys = lti(*system)._as_ss()
2402 if N is None:
2403 N = 100
2404 if T is None:
2405 T = _default_response_times(sys.A, N)
2406 else:
2407 T = asarray(T)
2408 U = ones(T.shape, sys.A.dtype)
2409 vals = lsim(sys, U, T, X0=X0, interp=False)
2410 return vals[0], vals[1]
2413def step2(system, X0=None, T=None, N=None, **kwargs):
2414 """Step response of continuous-time system.
2416 This function is functionally the same as `scipy.signal.step`, but
2417 it uses the function `scipy.signal.lsim2` to compute the step
2418 response.
2420 Parameters
2421 ----------
2422 system : an instance of the LTI class or a tuple of array_like
2423 describing the system.
2424 The following gives the number of elements in the tuple and
2425 the interpretation:
2427 * 1 (instance of `lti`)
2428 * 2 (num, den)
2429 * 3 (zeros, poles, gain)
2430 * 4 (A, B, C, D)
2432 X0 : array_like, optional
2433 Initial state-vector (default is zero).
2434 T : array_like, optional
2435 Time points (computed if not given).
2436 N : int, optional
2437 Number of time points to compute if `T` is not given.
2438 kwargs : various types
2439 Additional keyword arguments are passed on the function
2440 `scipy.signal.lsim2`, which in turn passes them on to
2441 `scipy.integrate.odeint`. See the documentation for
2442 `scipy.integrate.odeint` for information about these arguments.
2444 Returns
2445 -------
2446 T : 1D ndarray
2447 Output time points.
2448 yout : 1D ndarray
2449 Step response of system.
2451 See also
2452 --------
2453 scipy.signal.step
2455 Notes
2456 -----
2457 If (num, den) is passed in for ``system``, coefficients for both the
2458 numerator and denominator should be specified in descending exponent
2459 order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
2461 .. versionadded:: 0.8.0
2463 Examples
2464 --------
2465 >>> from scipy import signal
2466 >>> import matplotlib.pyplot as plt
2467 >>> lti = signal.lti([1.0], [1.0, 1.0])
2468 >>> t, y = signal.step2(lti)
2469 >>> plt.plot(t, y)
2470 >>> plt.xlabel('Time [s]')
2471 >>> plt.ylabel('Amplitude')
2472 >>> plt.title('Step response for 1. Order Lowpass')
2473 >>> plt.grid()
2475 """
2476 if isinstance(system, lti):
2477 sys = system._as_ss()
2478 elif isinstance(system, dlti):
2479 raise AttributeError('step2 can only be used with continuous-time '
2480 'systems.')
2481 else:
2482 sys = lti(*system)._as_ss()
2483 if N is None:
2484 N = 100
2485 if T is None:
2486 T = _default_response_times(sys.A, N)
2487 else:
2488 T = asarray(T)
2489 U = ones(T.shape, sys.A.dtype)
2490 vals = lsim2(sys, U, T, X0=X0, **kwargs)
2491 return vals[0], vals[1]
2494def bode(system, w=None, n=100):
2495 """
2496 Calculate Bode magnitude and phase data of a continuous-time system.
2498 Parameters
2499 ----------
2500 system : an instance of the LTI class or a tuple describing the system.
2501 The following gives the number of elements in the tuple and
2502 the interpretation:
2504 * 1 (instance of `lti`)
2505 * 2 (num, den)
2506 * 3 (zeros, poles, gain)
2507 * 4 (A, B, C, D)
2509 w : array_like, optional
2510 Array of frequencies (in rad/s). Magnitude and phase data is calculated
2511 for every value in this array. If not given a reasonable set will be
2512 calculated.
2513 n : int, optional
2514 Number of frequency points to compute if `w` is not given. The `n`
2515 frequencies are logarithmically spaced in an interval chosen to
2516 include the influence of the poles and zeros of the system.
2518 Returns
2519 -------
2520 w : 1D ndarray
2521 Frequency array [rad/s]
2522 mag : 1D ndarray
2523 Magnitude array [dB]
2524 phase : 1D ndarray
2525 Phase array [deg]
2527 Notes
2528 -----
2529 If (num, den) is passed in for ``system``, coefficients for both the
2530 numerator and denominator should be specified in descending exponent
2531 order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
2533 .. versionadded:: 0.11.0
2535 Examples
2536 --------
2537 >>> from scipy import signal
2538 >>> import matplotlib.pyplot as plt
2540 >>> sys = signal.TransferFunction([1], [1, 1])
2541 >>> w, mag, phase = signal.bode(sys)
2543 >>> plt.figure()
2544 >>> plt.semilogx(w, mag) # Bode magnitude plot
2545 >>> plt.figure()
2546 >>> plt.semilogx(w, phase) # Bode phase plot
2547 >>> plt.show()
2549 """
2550 w, y = freqresp(system, w=w, n=n)
2552 mag = 20.0 * numpy.log10(abs(y))
2553 phase = numpy.unwrap(numpy.arctan2(y.imag, y.real)) * 180.0 / numpy.pi
2555 return w, mag, phase
2558def freqresp(system, w=None, n=10000):
2559 """Calculate the frequency response of a continuous-time system.
2561 Parameters
2562 ----------
2563 system : an instance of the `lti` class or a tuple describing the system.
2564 The following gives the number of elements in the tuple and
2565 the interpretation:
2567 * 1 (instance of `lti`)
2568 * 2 (num, den)
2569 * 3 (zeros, poles, gain)
2570 * 4 (A, B, C, D)
2572 w : array_like, optional
2573 Array of frequencies (in rad/s). Magnitude and phase data is
2574 calculated for every value in this array. If not given, a reasonable
2575 set will be calculated.
2576 n : int, optional
2577 Number of frequency points to compute if `w` is not given. The `n`
2578 frequencies are logarithmically spaced in an interval chosen to
2579 include the influence of the poles and zeros of the system.
2581 Returns
2582 -------
2583 w : 1D ndarray
2584 Frequency array [rad/s]
2585 H : 1D ndarray
2586 Array of complex magnitude values
2588 Notes
2589 -----
2590 If (num, den) is passed in for ``system``, coefficients for both the
2591 numerator and denominator should be specified in descending exponent
2592 order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
2594 Examples
2595 --------
2596 Generating the Nyquist plot of a transfer function
2598 >>> from scipy import signal
2599 >>> import matplotlib.pyplot as plt
2601 Transfer function: H(s) = 5 / (s-1)^3
2603 >>> s1 = signal.ZerosPolesGain([], [1, 1, 1], [5])
2605 >>> w, H = signal.freqresp(s1)
2607 >>> plt.figure()
2608 >>> plt.plot(H.real, H.imag, "b")
2609 >>> plt.plot(H.real, -H.imag, "r")
2610 >>> plt.show()
2611 """
2612 if isinstance(system, lti):
2613 if isinstance(system, (TransferFunction, ZerosPolesGain)):
2614 sys = system
2615 else:
2616 sys = system._as_zpk()
2617 elif isinstance(system, dlti):
2618 raise AttributeError('freqresp can only be used with continuous-time '
2619 'systems.')
2620 else:
2621 sys = lti(*system)._as_zpk()
2623 if sys.inputs != 1 or sys.outputs != 1:
2624 raise ValueError("freqresp() requires a SISO (single input, single "
2625 "output) system.")
2627 if w is not None:
2628 worN = w
2629 else:
2630 worN = n
2632 if isinstance(sys, TransferFunction):
2633 # In the call to freqs(), sys.num.ravel() is used because there are
2634 # cases where sys.num is a 2-D array with a single row.
2635 w, h = freqs(sys.num.ravel(), sys.den, worN=worN)
2637 elif isinstance(sys, ZerosPolesGain):
2638 w, h = freqs_zpk(sys.zeros, sys.poles, sys.gain, worN=worN)
2640 return w, h
2643# This class will be used by place_poles to return its results
2644# see https://code.activestate.com/recipes/52308/
2645class Bunch:
2646 def __init__(self, **kwds):
2647 self.__dict__.update(kwds)
2650def _valid_inputs(A, B, poles, method, rtol, maxiter):
2651 """
2652 Check the poles come in complex conjugage pairs
2653 Check shapes of A, B and poles are compatible.
2654 Check the method chosen is compatible with provided poles
2655 Return update method to use and ordered poles
2657 """
2658 poles = np.asarray(poles)
2659 if poles.ndim > 1:
2660 raise ValueError("Poles must be a 1D array like.")
2661 # Will raise ValueError if poles do not come in complex conjugates pairs
2662 poles = _order_complex_poles(poles)
2663 if A.ndim > 2:
2664 raise ValueError("A must be a 2D array/matrix.")
2665 if B.ndim > 2:
2666 raise ValueError("B must be a 2D array/matrix")
2667 if A.shape[0] != A.shape[1]:
2668 raise ValueError("A must be square")
2669 if len(poles) > A.shape[0]:
2670 raise ValueError("maximum number of poles is %d but you asked for %d" %
2671 (A.shape[0], len(poles)))
2672 if len(poles) < A.shape[0]:
2673 raise ValueError("number of poles is %d but you should provide %d" %
2674 (len(poles), A.shape[0]))
2675 r = np.linalg.matrix_rank(B)
2676 for p in poles:
2677 if sum(p == poles) > r:
2678 raise ValueError("at least one of the requested pole is repeated "
2679 "more than rank(B) times")
2680 # Choose update method
2681 update_loop = _YT_loop
2682 if method not in ('KNV0','YT'):
2683 raise ValueError("The method keyword must be one of 'YT' or 'KNV0'")
2685 if method == "KNV0":
2686 update_loop = _KNV0_loop
2687 if not all(np.isreal(poles)):
2688 raise ValueError("Complex poles are not supported by KNV0")
2690 if maxiter < 1:
2691 raise ValueError("maxiter must be at least equal to 1")
2693 # We do not check rtol <= 0 as the user can use a negative rtol to
2694 # force maxiter iterations
2695 if rtol > 1:
2696 raise ValueError("rtol can not be greater than 1")
2698 return update_loop, poles
2701def _order_complex_poles(poles):
2702 """
2703 Check we have complex conjugates pairs and reorder P according to YT, ie
2704 real_poles, complex_i, conjugate complex_i, ....
2705 The lexicographic sort on the complex poles is added to help the user to
2706 compare sets of poles.
2707 """
2708 ordered_poles = np.sort(poles[np.isreal(poles)])
2709 im_poles = []
2710 for p in np.sort(poles[np.imag(poles) < 0]):
2711 if np.conj(p) in poles:
2712 im_poles.extend((p, np.conj(p)))
2714 ordered_poles = np.hstack((ordered_poles, im_poles))
2716 if poles.shape[0] != len(ordered_poles):
2717 raise ValueError("Complex poles must come with their conjugates")
2718 return ordered_poles
2721def _KNV0(B, ker_pole, transfer_matrix, j, poles):
2722 """
2723 Algorithm "KNV0" Kautsky et Al. Robust pole
2724 assignment in linear state feedback, Int journal of Control
2725 1985, vol 41 p 1129->1155
2726 https://la.epfl.ch/files/content/sites/la/files/
2727 users/105941/public/KautskyNicholsDooren
2729 """
2730 # Remove xj form the base
2731 transfer_matrix_not_j = np.delete(transfer_matrix, j, axis=1)
2732 # If we QR this matrix in full mode Q=Q0|Q1
2733 # then Q1 will be a single column orthogonnal to
2734 # Q0, that's what we are looking for !
2736 # After merge of gh-4249 great speed improvements could be achieved
2737 # using QR updates instead of full QR in the line below
2739 # To debug with numpy qr uncomment the line below
2740 # Q, R = np.linalg.qr(transfer_matrix_not_j, mode="complete")
2741 Q, R = s_qr(transfer_matrix_not_j, mode="full")
2743 mat_ker_pj = np.dot(ker_pole[j], ker_pole[j].T)
2744 yj = np.dot(mat_ker_pj, Q[:, -1])
2746 # If Q[:, -1] is "almost" orthogonal to ker_pole[j] its
2747 # projection into ker_pole[j] will yield a vector
2748 # close to 0. As we are looking for a vector in ker_pole[j]
2749 # simply stick with transfer_matrix[:, j] (unless someone provides me with
2750 # a better choice ?)
2752 if not np.allclose(yj, 0):
2753 xj = yj/np.linalg.norm(yj)
2754 transfer_matrix[:, j] = xj
2756 # KNV does not support complex poles, using YT technique the two lines
2757 # below seem to work 9 out of 10 times but it is not reliable enough:
2758 # transfer_matrix[:, j]=real(xj)
2759 # transfer_matrix[:, j+1]=imag(xj)
2761 # Add this at the beginning of this function if you wish to test
2762 # complex support:
2763 # if ~np.isreal(P[j]) and (j>=B.shape[0]-1 or P[j]!=np.conj(P[j+1])):
2764 # return
2765 # Problems arise when imag(xj)=>0 I have no idea on how to fix this
2768def _YT_real(ker_pole, Q, transfer_matrix, i, j):
2769 """
2770 Applies algorithm from YT section 6.1 page 19 related to real pairs
2771 """
2772 # step 1 page 19
2773 u = Q[:, -2, np.newaxis]
2774 v = Q[:, -1, np.newaxis]
2776 # step 2 page 19
2777 m = np.dot(np.dot(ker_pole[i].T, np.dot(u, v.T) -
2778 np.dot(v, u.T)), ker_pole[j])
2780 # step 3 page 19
2781 um, sm, vm = np.linalg.svd(m)
2782 # mu1, mu2 two first columns of U => 2 first lines of U.T
2783 mu1, mu2 = um.T[:2, :, np.newaxis]
2784 # VM is V.T with numpy we want the first two lines of V.T
2785 nu1, nu2 = vm[:2, :, np.newaxis]
2787 # what follows is a rough python translation of the formulas
2788 # in section 6.2 page 20 (step 4)
2789 transfer_matrix_j_mo_transfer_matrix_j = np.vstack((
2790 transfer_matrix[:, i, np.newaxis],
2791 transfer_matrix[:, j, np.newaxis]))
2793 if not np.allclose(sm[0], sm[1]):
2794 ker_pole_imo_mu1 = np.dot(ker_pole[i], mu1)
2795 ker_pole_i_nu1 = np.dot(ker_pole[j], nu1)
2796 ker_pole_mu_nu = np.vstack((ker_pole_imo_mu1, ker_pole_i_nu1))
2797 else:
2798 ker_pole_ij = np.vstack((
2799 np.hstack((ker_pole[i],
2800 np.zeros(ker_pole[i].shape))),
2801 np.hstack((np.zeros(ker_pole[j].shape),
2802 ker_pole[j]))
2803 ))
2804 mu_nu_matrix = np.vstack(
2805 (np.hstack((mu1, mu2)), np.hstack((nu1, nu2)))
2806 )
2807 ker_pole_mu_nu = np.dot(ker_pole_ij, mu_nu_matrix)
2808 transfer_matrix_ij = np.dot(np.dot(ker_pole_mu_nu, ker_pole_mu_nu.T),
2809 transfer_matrix_j_mo_transfer_matrix_j)
2810 if not np.allclose(transfer_matrix_ij, 0):
2811 transfer_matrix_ij = (np.sqrt(2)*transfer_matrix_ij /
2812 np.linalg.norm(transfer_matrix_ij))
2813 transfer_matrix[:, i] = transfer_matrix_ij[
2814 :transfer_matrix[:, i].shape[0], 0
2815 ]
2816 transfer_matrix[:, j] = transfer_matrix_ij[
2817 transfer_matrix[:, i].shape[0]:, 0
2818 ]
2819 else:
2820 # As in knv0 if transfer_matrix_j_mo_transfer_matrix_j is orthogonal to
2821 # Vect{ker_pole_mu_nu} assign transfer_matrixi/transfer_matrix_j to
2822 # ker_pole_mu_nu and iterate. As we are looking for a vector in
2823 # Vect{Matker_pole_MU_NU} (see section 6.1 page 19) this might help
2824 # (that's a guess, not a claim !)
2825 transfer_matrix[:, i] = ker_pole_mu_nu[
2826 :transfer_matrix[:, i].shape[0], 0
2827 ]
2828 transfer_matrix[:, j] = ker_pole_mu_nu[
2829 transfer_matrix[:, i].shape[0]:, 0
2830 ]
2833def _YT_complex(ker_pole, Q, transfer_matrix, i, j):
2834 """
2835 Applies algorithm from YT section 6.2 page 20 related to complex pairs
2836 """
2837 # step 1 page 20
2838 ur = np.sqrt(2)*Q[:, -2, np.newaxis]
2839 ui = np.sqrt(2)*Q[:, -1, np.newaxis]
2840 u = ur + 1j*ui
2842 # step 2 page 20
2843 ker_pole_ij = ker_pole[i]
2844 m = np.dot(np.dot(np.conj(ker_pole_ij.T), np.dot(u, np.conj(u).T) -
2845 np.dot(np.conj(u), u.T)), ker_pole_ij)
2847 # step 3 page 20
2848 e_val, e_vec = np.linalg.eig(m)
2849 # sort eigenvalues according to their module
2850 e_val_idx = np.argsort(np.abs(e_val))
2851 mu1 = e_vec[:, e_val_idx[-1], np.newaxis]
2852 mu2 = e_vec[:, e_val_idx[-2], np.newaxis]
2854 # what follows is a rough python translation of the formulas
2855 # in section 6.2 page 20 (step 4)
2857 # remember transfer_matrix_i has been split as
2858 # transfer_matrix[i]=real(transfer_matrix_i) and
2859 # transfer_matrix[j]=imag(transfer_matrix_i)
2860 transfer_matrix_j_mo_transfer_matrix_j = (
2861 transfer_matrix[:, i, np.newaxis] +
2862 1j*transfer_matrix[:, j, np.newaxis]
2863 )
2864 if not np.allclose(np.abs(e_val[e_val_idx[-1]]),
2865 np.abs(e_val[e_val_idx[-2]])):
2866 ker_pole_mu = np.dot(ker_pole_ij, mu1)
2867 else:
2868 mu1_mu2_matrix = np.hstack((mu1, mu2))
2869 ker_pole_mu = np.dot(ker_pole_ij, mu1_mu2_matrix)
2870 transfer_matrix_i_j = np.dot(np.dot(ker_pole_mu, np.conj(ker_pole_mu.T)),
2871 transfer_matrix_j_mo_transfer_matrix_j)
2873 if not np.allclose(transfer_matrix_i_j, 0):
2874 transfer_matrix_i_j = (transfer_matrix_i_j /
2875 np.linalg.norm(transfer_matrix_i_j))
2876 transfer_matrix[:, i] = np.real(transfer_matrix_i_j[:, 0])
2877 transfer_matrix[:, j] = np.imag(transfer_matrix_i_j[:, 0])
2878 else:
2879 # same idea as in YT_real
2880 transfer_matrix[:, i] = np.real(ker_pole_mu[:, 0])
2881 transfer_matrix[:, j] = np.imag(ker_pole_mu[:, 0])
2884def _YT_loop(ker_pole, transfer_matrix, poles, B, maxiter, rtol):
2885 """
2886 Algorithm "YT" Tits, Yang. Globally Convergent
2887 Algorithms for Robust Pole Assignment by State Feedback
2888 https://hdl.handle.net/1903/5598
2889 The poles P have to be sorted accordingly to section 6.2 page 20
2891 """
2892 # The IEEE edition of the YT paper gives useful information on the
2893 # optimal update order for the real poles in order to minimize the number
2894 # of times we have to loop over all poles, see page 1442
2895 nb_real = poles[np.isreal(poles)].shape[0]
2896 # hnb => Half Nb Real
2897 hnb = nb_real // 2
2899 # Stick to the indices in the paper and then remove one to get numpy array
2900 # index it is a bit easier to link the code to the paper this way even if it
2901 # is not very clean. The paper is unclear about what should be done when
2902 # there is only one real pole => use KNV0 on this real pole seem to work
2903 if nb_real > 0:
2904 #update the biggest real pole with the smallest one
2905 update_order = [[nb_real], [1]]
2906 else:
2907 update_order = [[],[]]
2909 r_comp = np.arange(nb_real+1, len(poles)+1, 2)
2910 # step 1.a
2911 r_p = np.arange(1, hnb+nb_real % 2)
2912 update_order[0].extend(2*r_p)
2913 update_order[1].extend(2*r_p+1)
2914 # step 1.b
2915 update_order[0].extend(r_comp)
2916 update_order[1].extend(r_comp+1)
2917 # step 1.c
2918 r_p = np.arange(1, hnb+1)
2919 update_order[0].extend(2*r_p-1)
2920 update_order[1].extend(2*r_p)
2921 # step 1.d
2922 if hnb == 0 and np.isreal(poles[0]):
2923 update_order[0].append(1)
2924 update_order[1].append(1)
2925 update_order[0].extend(r_comp)
2926 update_order[1].extend(r_comp+1)
2927 # step 2.a
2928 r_j = np.arange(2, hnb+nb_real % 2)
2929 for j in r_j:
2930 for i in range(1, hnb+1):
2931 update_order[0].append(i)
2932 update_order[1].append(i+j)
2933 # step 2.b
2934 if hnb == 0 and np.isreal(poles[0]):
2935 update_order[0].append(1)
2936 update_order[1].append(1)
2937 update_order[0].extend(r_comp)
2938 update_order[1].extend(r_comp+1)
2939 # step 2.c
2940 r_j = np.arange(2, hnb+nb_real % 2)
2941 for j in r_j:
2942 for i in range(hnb+1, nb_real+1):
2943 idx_1 = i+j
2944 if idx_1 > nb_real:
2945 idx_1 = i+j-nb_real
2946 update_order[0].append(i)
2947 update_order[1].append(idx_1)
2948 # step 2.d
2949 if hnb == 0 and np.isreal(poles[0]):
2950 update_order[0].append(1)
2951 update_order[1].append(1)
2952 update_order[0].extend(r_comp)
2953 update_order[1].extend(r_comp+1)
2954 # step 3.a
2955 for i in range(1, hnb+1):
2956 update_order[0].append(i)
2957 update_order[1].append(i+hnb)
2958 # step 3.b
2959 if hnb == 0 and np.isreal(poles[0]):
2960 update_order[0].append(1)
2961 update_order[1].append(1)
2962 update_order[0].extend(r_comp)
2963 update_order[1].extend(r_comp+1)
2965 update_order = np.array(update_order).T-1
2966 stop = False
2967 nb_try = 0
2968 while nb_try < maxiter and not stop:
2969 det_transfer_matrixb = np.abs(np.linalg.det(transfer_matrix))
2970 for i, j in update_order:
2971 if i == j:
2972 assert i == 0, "i!=0 for KNV call in YT"
2973 assert np.isreal(poles[i]), "calling KNV on a complex pole"
2974 _KNV0(B, ker_pole, transfer_matrix, i, poles)
2975 else:
2976 transfer_matrix_not_i_j = np.delete(transfer_matrix, (i, j),
2977 axis=1)
2978 # after merge of gh-4249 great speed improvements could be
2979 # achieved using QR updates instead of full QR in the line below
2981 #to debug with numpy qr uncomment the line below
2982 #Q, _ = np.linalg.qr(transfer_matrix_not_i_j, mode="complete")
2983 Q, _ = s_qr(transfer_matrix_not_i_j, mode="full")
2985 if np.isreal(poles[i]):
2986 assert np.isreal(poles[j]), "mixing real and complex " + \
2987 "in YT_real" + str(poles)
2988 _YT_real(ker_pole, Q, transfer_matrix, i, j)
2989 else:
2990 assert ~np.isreal(poles[i]), "mixing real and complex " + \
2991 "in YT_real" + str(poles)
2992 _YT_complex(ker_pole, Q, transfer_matrix, i, j)
2994 det_transfer_matrix = np.max((np.sqrt(np.spacing(1)),
2995 np.abs(np.linalg.det(transfer_matrix))))
2996 cur_rtol = np.abs(
2997 (det_transfer_matrix -
2998 det_transfer_matrixb) /
2999 det_transfer_matrix)
3000 if cur_rtol < rtol and det_transfer_matrix > np.sqrt(np.spacing(1)):
3001 # Convergence test from YT page 21
3002 stop = True
3003 nb_try += 1
3004 return stop, cur_rtol, nb_try
3007def _KNV0_loop(ker_pole, transfer_matrix, poles, B, maxiter, rtol):
3008 """
3009 Loop over all poles one by one and apply KNV method 0 algorithm
3010 """
3011 # This method is useful only because we need to be able to call
3012 # _KNV0 from YT without looping over all poles, otherwise it would
3013 # have been fine to mix _KNV0_loop and _KNV0 in a single function
3014 stop = False
3015 nb_try = 0
3016 while nb_try < maxiter and not stop:
3017 det_transfer_matrixb = np.abs(np.linalg.det(transfer_matrix))
3018 for j in range(B.shape[0]):
3019 _KNV0(B, ker_pole, transfer_matrix, j, poles)
3021 det_transfer_matrix = np.max((np.sqrt(np.spacing(1)),
3022 np.abs(np.linalg.det(transfer_matrix))))
3023 cur_rtol = np.abs((det_transfer_matrix - det_transfer_matrixb) /
3024 det_transfer_matrix)
3025 if cur_rtol < rtol and det_transfer_matrix > np.sqrt(np.spacing(1)):
3026 # Convergence test from YT page 21
3027 stop = True
3029 nb_try += 1
3030 return stop, cur_rtol, nb_try
3033def place_poles(A, B, poles, method="YT", rtol=1e-3, maxiter=30):
3034 """
3035 Compute K such that eigenvalues (A - dot(B, K))=poles.
3037 K is the gain matrix such as the plant described by the linear system
3038 ``AX+BU`` will have its closed-loop poles, i.e the eigenvalues ``A - B*K``,
3039 as close as possible to those asked for in poles.
3041 SISO, MISO and MIMO systems are supported.
3043 Parameters
3044 ----------
3045 A, B : ndarray
3046 State-space representation of linear system ``AX + BU``.
3047 poles : array_like
3048 Desired real poles and/or complex conjugates poles.
3049 Complex poles are only supported with ``method="YT"`` (default).
3050 method: {'YT', 'KNV0'}, optional
3051 Which method to choose to find the gain matrix K. One of:
3053 - 'YT': Yang Tits
3054 - 'KNV0': Kautsky, Nichols, Van Dooren update method 0
3056 See References and Notes for details on the algorithms.
3057 rtol: float, optional
3058 After each iteration the determinant of the eigenvectors of
3059 ``A - B*K`` is compared to its previous value, when the relative
3060 error between these two values becomes lower than `rtol` the algorithm
3061 stops. Default is 1e-3.
3062 maxiter: int, optional
3063 Maximum number of iterations to compute the gain matrix.
3064 Default is 30.
3066 Returns
3067 -------
3068 full_state_feedback : Bunch object
3069 full_state_feedback is composed of:
3070 gain_matrix : 1-D ndarray
3071 The closed loop matrix K such as the eigenvalues of ``A-BK``
3072 are as close as possible to the requested poles.
3073 computed_poles : 1-D ndarray
3074 The poles corresponding to ``A-BK`` sorted as first the real
3075 poles in increasing order, then the complex congugates in
3076 lexicographic order.
3077 requested_poles : 1-D ndarray
3078 The poles the algorithm was asked to place sorted as above,
3079 they may differ from what was achieved.
3080 X : 2-D ndarray
3081 The transfer matrix such as ``X * diag(poles) = (A - B*K)*X``
3082 (see Notes)
3083 rtol : float
3084 The relative tolerance achieved on ``det(X)`` (see Notes).
3085 `rtol` will be NaN if it is possible to solve the system
3086 ``diag(poles) = (A - B*K)``, or 0 when the optimization
3087 algorithms can't do anything i.e when ``B.shape[1] == 1``.
3088 nb_iter : int
3089 The number of iterations performed before converging.
3090 `nb_iter` will be NaN if it is possible to solve the system
3091 ``diag(poles) = (A - B*K)``, or 0 when the optimization
3092 algorithms can't do anything i.e when ``B.shape[1] == 1``.
3094 Notes
3095 -----
3096 The Tits and Yang (YT), [2]_ paper is an update of the original Kautsky et
3097 al. (KNV) paper [1]_. KNV relies on rank-1 updates to find the transfer
3098 matrix X such that ``X * diag(poles) = (A - B*K)*X``, whereas YT uses
3099 rank-2 updates. This yields on average more robust solutions (see [2]_
3100 pp 21-22), furthermore the YT algorithm supports complex poles whereas KNV
3101 does not in its original version. Only update method 0 proposed by KNV has
3102 been implemented here, hence the name ``'KNV0'``.
3104 KNV extended to complex poles is used in Matlab's ``place`` function, YT is
3105 distributed under a non-free licence by Slicot under the name ``robpole``.
3106 It is unclear and undocumented how KNV0 has been extended to complex poles
3107 (Tits and Yang claim on page 14 of their paper that their method can not be
3108 used to extend KNV to complex poles), therefore only YT supports them in
3109 this implementation.
3111 As the solution to the problem of pole placement is not unique for MIMO
3112 systems, both methods start with a tentative transfer matrix which is
3113 altered in various way to increase its determinant. Both methods have been
3114 proven to converge to a stable solution, however depending on the way the
3115 initial transfer matrix is chosen they will converge to different
3116 solutions and therefore there is absolutely no guarantee that using
3117 ``'KNV0'`` will yield results similar to Matlab's or any other
3118 implementation of these algorithms.
3120 Using the default method ``'YT'`` should be fine in most cases; ``'KNV0'``
3121 is only provided because it is needed by ``'YT'`` in some specific cases.
3122 Furthermore ``'YT'`` gives on average more robust results than ``'KNV0'``
3123 when ``abs(det(X))`` is used as a robustness indicator.
3125 [2]_ is available as a technical report on the following URL:
3126 https://hdl.handle.net/1903/5598
3128 References
3129 ----------
3130 .. [1] J. Kautsky, N.K. Nichols and P. van Dooren, "Robust pole assignment
3131 in linear state feedback", International Journal of Control, Vol. 41
3132 pp. 1129-1155, 1985.
3133 .. [2] A.L. Tits and Y. Yang, "Globally convergent algorithms for robust
3134 pole assignment by state feedback", IEEE Transactions on Automatic
3135 Control, Vol. 41, pp. 1432-1452, 1996.
3137 Examples
3138 --------
3139 A simple example demonstrating real pole placement using both KNV and YT
3140 algorithms. This is example number 1 from section 4 of the reference KNV
3141 publication ([1]_):
3143 >>> from scipy import signal
3144 >>> import matplotlib.pyplot as plt
3146 >>> A = np.array([[ 1.380, -0.2077, 6.715, -5.676 ],
3147 ... [-0.5814, -4.290, 0, 0.6750 ],
3148 ... [ 1.067, 4.273, -6.654, 5.893 ],
3149 ... [ 0.0480, 4.273, 1.343, -2.104 ]])
3150 >>> B = np.array([[ 0, 5.679 ],
3151 ... [ 1.136, 1.136 ],
3152 ... [ 0, 0, ],
3153 ... [-3.146, 0 ]])
3154 >>> P = np.array([-0.2, -0.5, -5.0566, -8.6659])
3156 Now compute K with KNV method 0, with the default YT method and with the YT
3157 method while forcing 100 iterations of the algorithm and print some results
3158 after each call.
3160 >>> fsf1 = signal.place_poles(A, B, P, method='KNV0')
3161 >>> fsf1.gain_matrix
3162 array([[ 0.20071427, -0.96665799, 0.24066128, -0.10279785],
3163 [ 0.50587268, 0.57779091, 0.51795763, -0.41991442]])
3165 >>> fsf2 = signal.place_poles(A, B, P) # uses YT method
3166 >>> fsf2.computed_poles
3167 array([-8.6659, -5.0566, -0.5 , -0.2 ])
3169 >>> fsf3 = signal.place_poles(A, B, P, rtol=-1, maxiter=100)
3170 >>> fsf3.X
3171 array([[ 0.52072442+0.j, -0.08409372+0.j, -0.56847937+0.j, 0.74823657+0.j],
3172 [-0.04977751+0.j, -0.80872954+0.j, 0.13566234+0.j, -0.29322906+0.j],
3173 [-0.82266932+0.j, -0.19168026+0.j, -0.56348322+0.j, -0.43815060+0.j],
3174 [ 0.22267347+0.j, 0.54967577+0.j, -0.58387806+0.j, -0.40271926+0.j]])
3176 The absolute value of the determinant of X is a good indicator to check the
3177 robustness of the results, both ``'KNV0'`` and ``'YT'`` aim at maximizing
3178 it. Below a comparison of the robustness of the results above:
3180 >>> abs(np.linalg.det(fsf1.X)) < abs(np.linalg.det(fsf2.X))
3181 True
3182 >>> abs(np.linalg.det(fsf2.X)) < abs(np.linalg.det(fsf3.X))
3183 True
3185 Now a simple example for complex poles:
3187 >>> A = np.array([[ 0, 7/3., 0, 0 ],
3188 ... [ 0, 0, 0, 7/9. ],
3189 ... [ 0, 0, 0, 0 ],
3190 ... [ 0, 0, 0, 0 ]])
3191 >>> B = np.array([[ 0, 0 ],
3192 ... [ 0, 0 ],
3193 ... [ 1, 0 ],
3194 ... [ 0, 1 ]])
3195 >>> P = np.array([-3, -1, -2-1j, -2+1j]) / 3.
3196 >>> fsf = signal.place_poles(A, B, P, method='YT')
3198 We can plot the desired and computed poles in the complex plane:
3200 >>> t = np.linspace(0, 2*np.pi, 401)
3201 >>> plt.plot(np.cos(t), np.sin(t), 'k--') # unit circle
3202 >>> plt.plot(fsf.requested_poles.real, fsf.requested_poles.imag,
3203 ... 'wo', label='Desired')
3204 >>> plt.plot(fsf.computed_poles.real, fsf.computed_poles.imag, 'bx',
3205 ... label='Placed')
3206 >>> plt.grid()
3207 >>> plt.axis('image')
3208 >>> plt.axis([-1.1, 1.1, -1.1, 1.1])
3209 >>> plt.legend(bbox_to_anchor=(1.05, 1), loc=2, numpoints=1)
3211 """
3212 # Move away all the inputs checking, it only adds noise to the code
3213 update_loop, poles = _valid_inputs(A, B, poles, method, rtol, maxiter)
3215 # The current value of the relative tolerance we achieved
3216 cur_rtol = 0
3217 # The number of iterations needed before converging
3218 nb_iter = 0
3220 # Step A: QR decomposition of B page 1132 KN
3221 # to debug with numpy qr uncomment the line below
3222 # u, z = np.linalg.qr(B, mode="complete")
3223 u, z = s_qr(B, mode="full")
3224 rankB = np.linalg.matrix_rank(B)
3225 u0 = u[:, :rankB]
3226 u1 = u[:, rankB:]
3227 z = z[:rankB, :]
3229 # If we can use the identity matrix as X the solution is obvious
3230 if B.shape[0] == rankB:
3231 # if B is square and full rank there is only one solution
3232 # such as (A+BK)=inv(X)*diag(P)*X with X=eye(A.shape[0])
3233 # i.e K=inv(B)*(diag(P)-A)
3234 # if B has as many lines as its rank (but not square) there are many
3235 # solutions and we can choose one using least squares
3236 # => use lstsq in both cases.
3237 # In both cases the transfer matrix X will be eye(A.shape[0]) and I
3238 # can hardly think of a better one so there is nothing to optimize
3239 #
3240 # for complex poles we use the following trick
3241 #
3242 # |a -b| has for eigenvalues a+b and a-b
3243 # |b a|
3244 #
3245 # |a+bi 0| has the obvious eigenvalues a+bi and a-bi
3246 # |0 a-bi|
3247 #
3248 # e.g solving the first one in R gives the solution
3249 # for the second one in C
3250 diag_poles = np.zeros(A.shape)
3251 idx = 0
3252 while idx < poles.shape[0]:
3253 p = poles[idx]
3254 diag_poles[idx, idx] = np.real(p)
3255 if ~np.isreal(p):
3256 diag_poles[idx, idx+1] = -np.imag(p)
3257 diag_poles[idx+1, idx+1] = np.real(p)
3258 diag_poles[idx+1, idx] = np.imag(p)
3259 idx += 1 # skip next one
3260 idx += 1
3261 gain_matrix = np.linalg.lstsq(B, diag_poles-A, rcond=-1)[0]
3262 transfer_matrix = np.eye(A.shape[0])
3263 cur_rtol = np.nan
3264 nb_iter = np.nan
3265 else:
3266 # step A (p1144 KNV) and beginning of step F: decompose
3267 # dot(U1.T, A-P[i]*I).T and build our set of transfer_matrix vectors
3268 # in the same loop
3269 ker_pole = []
3271 # flag to skip the conjugate of a complex pole
3272 skip_conjugate = False
3273 # select orthonormal base ker_pole for each Pole and vectors for
3274 # transfer_matrix
3275 for j in range(B.shape[0]):
3276 if skip_conjugate:
3277 skip_conjugate = False
3278 continue
3279 pole_space_j = np.dot(u1.T, A-poles[j]*np.eye(B.shape[0])).T
3281 # after QR Q=Q0|Q1
3282 # only Q0 is used to reconstruct the qr'ed (dot Q, R) matrix.
3283 # Q1 is orthogonnal to Q0 and will be multiplied by the zeros in
3284 # R when using mode "complete". In default mode Q1 and the zeros
3285 # in R are not computed
3287 # To debug with numpy qr uncomment the line below
3288 # Q, _ = np.linalg.qr(pole_space_j, mode="complete")
3289 Q, _ = s_qr(pole_space_j, mode="full")
3291 ker_pole_j = Q[:, pole_space_j.shape[1]:]
3293 # We want to select one vector in ker_pole_j to build the transfer
3294 # matrix, however qr returns sometimes vectors with zeros on the
3295 # same line for each pole and this yields very long convergence
3296 # times.
3297 # Or some other times a set of vectors, one with zero imaginary
3298 # part and one (or several) with imaginary parts. After trying
3299 # many ways to select the best possible one (eg ditch vectors
3300 # with zero imaginary part for complex poles) I ended up summing
3301 # all vectors in ker_pole_j, this solves 100% of the problems and
3302 # is a valid choice for transfer_matrix.
3303 # This way for complex poles we are sure to have a non zero
3304 # imaginary part that way, and the problem of lines full of zeros
3305 # in transfer_matrix is solved too as when a vector from
3306 # ker_pole_j has a zero the other one(s) when
3307 # ker_pole_j.shape[1]>1) for sure won't have a zero there.
3309 transfer_matrix_j = np.sum(ker_pole_j, axis=1)[:, np.newaxis]
3310 transfer_matrix_j = (transfer_matrix_j /
3311 np.linalg.norm(transfer_matrix_j))
3312 if ~np.isreal(poles[j]): # complex pole
3313 transfer_matrix_j = np.hstack([np.real(transfer_matrix_j),
3314 np.imag(transfer_matrix_j)])
3315 ker_pole.extend([ker_pole_j, ker_pole_j])
3317 # Skip next pole as it is the conjugate
3318 skip_conjugate = True
3319 else: # real pole, nothing to do
3320 ker_pole.append(ker_pole_j)
3322 if j == 0:
3323 transfer_matrix = transfer_matrix_j
3324 else:
3325 transfer_matrix = np.hstack((transfer_matrix, transfer_matrix_j))
3327 if rankB > 1: # otherwise there is nothing we can optimize
3328 stop, cur_rtol, nb_iter = update_loop(ker_pole, transfer_matrix,
3329 poles, B, maxiter, rtol)
3330 if not stop and rtol > 0:
3331 # if rtol<=0 the user has probably done that on purpose,
3332 # don't annoy him
3333 err_msg = (
3334 "Convergence was not reached after maxiter iterations.\n"
3335 "You asked for a relative tolerance of %f we got %f" %
3336 (rtol, cur_rtol)
3337 )
3338 warnings.warn(err_msg)
3340 # reconstruct transfer_matrix to match complex conjugate pairs,
3341 # ie transfer_matrix_j/transfer_matrix_j+1 are
3342 # Re(Complex_pole), Im(Complex_pole) now and will be Re-Im/Re+Im after
3343 transfer_matrix = transfer_matrix.astype(complex)
3344 idx = 0
3345 while idx < poles.shape[0]-1:
3346 if ~np.isreal(poles[idx]):
3347 rel = transfer_matrix[:, idx].copy()
3348 img = transfer_matrix[:, idx+1]
3349 # rel will be an array referencing a column of transfer_matrix
3350 # if we don't copy() it will changer after the next line and
3351 # and the line after will not yield the correct value
3352 transfer_matrix[:, idx] = rel-1j*img
3353 transfer_matrix[:, idx+1] = rel+1j*img
3354 idx += 1 # skip next one
3355 idx += 1
3357 try:
3358 m = np.linalg.solve(transfer_matrix.T, np.dot(np.diag(poles),
3359 transfer_matrix.T)).T
3360 gain_matrix = np.linalg.solve(z, np.dot(u0.T, m-A))
3361 except np.linalg.LinAlgError:
3362 raise ValueError("The poles you've chosen can't be placed. "
3363 "Check the controllability matrix and try "
3364 "another set of poles")
3366 # Beware: Kautsky solves A+BK but the usual form is A-BK
3367 gain_matrix = -gain_matrix
3368 # K still contains complex with ~=0j imaginary parts, get rid of them
3369 gain_matrix = np.real(gain_matrix)
3371 full_state_feedback = Bunch()
3372 full_state_feedback.gain_matrix = gain_matrix
3373 full_state_feedback.computed_poles = _order_complex_poles(
3374 np.linalg.eig(A - np.dot(B, gain_matrix))[0]
3375 )
3376 full_state_feedback.requested_poles = poles
3377 full_state_feedback.X = transfer_matrix
3378 full_state_feedback.rtol = cur_rtol
3379 full_state_feedback.nb_iter = nb_iter
3381 return full_state_feedback
3384def dlsim(system, u, t=None, x0=None):
3385 """
3386 Simulate output of a discrete-time linear system.
3388 Parameters
3389 ----------
3390 system : tuple of array_like or instance of `dlti`
3391 A tuple describing the system.
3392 The following gives the number of elements in the tuple and
3393 the interpretation:
3395 * 1: (instance of `dlti`)
3396 * 3: (num, den, dt)
3397 * 4: (zeros, poles, gain, dt)
3398 * 5: (A, B, C, D, dt)
3400 u : array_like
3401 An input array describing the input at each time `t` (interpolation is
3402 assumed between given times). If there are multiple inputs, then each
3403 column of the rank-2 array represents an input.
3404 t : array_like, optional
3405 The time steps at which the input is defined. If `t` is given, it
3406 must be the same length as `u`, and the final value in `t` determines
3407 the number of steps returned in the output.
3408 x0 : array_like, optional
3409 The initial conditions on the state vector (zero by default).
3411 Returns
3412 -------
3413 tout : ndarray
3414 Time values for the output, as a 1-D array.
3415 yout : ndarray
3416 System response, as a 1-D array.
3417 xout : ndarray, optional
3418 Time-evolution of the state-vector. Only generated if the input is a
3419 `StateSpace` system.
3421 See Also
3422 --------
3423 lsim, dstep, dimpulse, cont2discrete
3425 Examples
3426 --------
3427 A simple integrator transfer function with a discrete time step of 1.0
3428 could be implemented as:
3430 >>> from scipy import signal
3431 >>> tf = ([1.0,], [1.0, -1.0], 1.0)
3432 >>> t_in = [0.0, 1.0, 2.0, 3.0]
3433 >>> u = np.asarray([0.0, 0.0, 1.0, 1.0])
3434 >>> t_out, y = signal.dlsim(tf, u, t=t_in)
3435 >>> y.T
3436 array([[ 0., 0., 0., 1.]])
3438 """
3439 # Convert system to dlti-StateSpace
3440 if isinstance(system, lti):
3441 raise AttributeError('dlsim can only be used with discrete-time dlti '
3442 'systems.')
3443 elif not isinstance(system, dlti):
3444 system = dlti(*system[:-1], dt=system[-1])
3446 # Condition needed to ensure output remains compatible
3447 is_ss_input = isinstance(system, StateSpace)
3448 system = system._as_ss()
3450 u = np.atleast_1d(u)
3452 if u.ndim == 1:
3453 u = np.atleast_2d(u).T
3455 if t is None:
3456 out_samples = len(u)
3457 stoptime = (out_samples - 1) * system.dt
3458 else:
3459 stoptime = t[-1]
3460 out_samples = int(np.floor(stoptime / system.dt)) + 1
3462 # Pre-build output arrays
3463 xout = np.zeros((out_samples, system.A.shape[0]))
3464 yout = np.zeros((out_samples, system.C.shape[0]))
3465 tout = np.linspace(0.0, stoptime, num=out_samples)
3467 # Check initial condition
3468 if x0 is None:
3469 xout[0, :] = np.zeros((system.A.shape[1],))
3470 else:
3471 xout[0, :] = np.asarray(x0)
3473 # Pre-interpolate inputs into the desired time steps
3474 if t is None:
3475 u_dt = u
3476 else:
3477 if len(u.shape) == 1:
3478 u = u[:, np.newaxis]
3480 u_dt_interp = interp1d(t, u.transpose(), copy=False, bounds_error=True)
3481 u_dt = u_dt_interp(tout).transpose()
3483 # Simulate the system
3484 for i in range(0, out_samples - 1):
3485 xout[i+1, :] = (np.dot(system.A, xout[i, :]) +
3486 np.dot(system.B, u_dt[i, :]))
3487 yout[i, :] = (np.dot(system.C, xout[i, :]) +
3488 np.dot(system.D, u_dt[i, :]))
3490 # Last point
3491 yout[out_samples-1, :] = (np.dot(system.C, xout[out_samples-1, :]) +
3492 np.dot(system.D, u_dt[out_samples-1, :]))
3494 if is_ss_input:
3495 return tout, yout, xout
3496 else:
3497 return tout, yout
3500def dimpulse(system, x0=None, t=None, n=None):
3501 """
3502 Impulse response of discrete-time system.
3504 Parameters
3505 ----------
3506 system : tuple of array_like or instance of `dlti`
3507 A tuple describing the system.
3508 The following gives the number of elements in the tuple and
3509 the interpretation:
3511 * 1: (instance of `dlti`)
3512 * 3: (num, den, dt)
3513 * 4: (zeros, poles, gain, dt)
3514 * 5: (A, B, C, D, dt)
3516 x0 : array_like, optional
3517 Initial state-vector. Defaults to zero.
3518 t : array_like, optional
3519 Time points. Computed if not given.
3520 n : int, optional
3521 The number of time points to compute (if `t` is not given).
3523 Returns
3524 -------
3525 tout : ndarray
3526 Time values for the output, as a 1-D array.
3527 yout : tuple of ndarray
3528 Impulse response of system. Each element of the tuple represents
3529 the output of the system based on an impulse in each input.
3531 See Also
3532 --------
3533 impulse, dstep, dlsim, cont2discrete
3535 Examples
3536 --------
3537 >>> from scipy import signal
3538 >>> import matplotlib.pyplot as plt
3540 >>> butter = signal.dlti(*signal.butter(3, 0.5))
3541 >>> t, y = signal.dimpulse(butter, n=25)
3542 >>> plt.step(t, np.squeeze(y))
3543 >>> plt.grid()
3544 >>> plt.xlabel('n [samples]')
3545 >>> plt.ylabel('Amplitude')
3547 """
3548 # Convert system to dlti-StateSpace
3549 if isinstance(system, dlti):
3550 system = system._as_ss()
3551 elif isinstance(system, lti):
3552 raise AttributeError('dimpulse can only be used with discrete-time '
3553 'dlti systems.')
3554 else:
3555 system = dlti(*system[:-1], dt=system[-1])._as_ss()
3557 # Default to 100 samples if unspecified
3558 if n is None:
3559 n = 100
3561 # If time is not specified, use the number of samples
3562 # and system dt
3563 if t is None:
3564 t = np.linspace(0, n * system.dt, n, endpoint=False)
3565 else:
3566 t = np.asarray(t)
3568 # For each input, implement a step change
3569 yout = None
3570 for i in range(0, system.inputs):
3571 u = np.zeros((t.shape[0], system.inputs))
3572 u[0, i] = 1.0
3574 one_output = dlsim(system, u, t=t, x0=x0)
3576 if yout is None:
3577 yout = (one_output[1],)
3578 else:
3579 yout = yout + (one_output[1],)
3581 tout = one_output[0]
3583 return tout, yout
3586def dstep(system, x0=None, t=None, n=None):
3587 """
3588 Step response of discrete-time system.
3590 Parameters
3591 ----------
3592 system : tuple of array_like
3593 A tuple describing the system.
3594 The following gives the number of elements in the tuple and
3595 the interpretation:
3597 * 1: (instance of `dlti`)
3598 * 3: (num, den, dt)
3599 * 4: (zeros, poles, gain, dt)
3600 * 5: (A, B, C, D, dt)
3602 x0 : array_like, optional
3603 Initial state-vector. Defaults to zero.
3604 t : array_like, optional
3605 Time points. Computed if not given.
3606 n : int, optional
3607 The number of time points to compute (if `t` is not given).
3609 Returns
3610 -------
3611 tout : ndarray
3612 Output time points, as a 1-D array.
3613 yout : tuple of ndarray
3614 Step response of system. Each element of the tuple represents
3615 the output of the system based on a step response to each input.
3617 See Also
3618 --------
3619 step, dimpulse, dlsim, cont2discrete
3621 Examples
3622 --------
3623 >>> from scipy import signal
3624 >>> import matplotlib.pyplot as plt
3626 >>> butter = signal.dlti(*signal.butter(3, 0.5))
3627 >>> t, y = signal.dstep(butter, n=25)
3628 >>> plt.step(t, np.squeeze(y))
3629 >>> plt.grid()
3630 >>> plt.xlabel('n [samples]')
3631 >>> plt.ylabel('Amplitude')
3632 """
3633 # Convert system to dlti-StateSpace
3634 if isinstance(system, dlti):
3635 system = system._as_ss()
3636 elif isinstance(system, lti):
3637 raise AttributeError('dstep can only be used with discrete-time dlti '
3638 'systems.')
3639 else:
3640 system = dlti(*system[:-1], dt=system[-1])._as_ss()
3642 # Default to 100 samples if unspecified
3643 if n is None:
3644 n = 100
3646 # If time is not specified, use the number of samples
3647 # and system dt
3648 if t is None:
3649 t = np.linspace(0, n * system.dt, n, endpoint=False)
3650 else:
3651 t = np.asarray(t)
3653 # For each input, implement a step change
3654 yout = None
3655 for i in range(0, system.inputs):
3656 u = np.zeros((t.shape[0], system.inputs))
3657 u[:, i] = np.ones((t.shape[0],))
3659 one_output = dlsim(system, u, t=t, x0=x0)
3661 if yout is None:
3662 yout = (one_output[1],)
3663 else:
3664 yout = yout + (one_output[1],)
3666 tout = one_output[0]
3668 return tout, yout
3671def dfreqresp(system, w=None, n=10000, whole=False):
3672 """
3673 Calculate the frequency response of a discrete-time system.
3675 Parameters
3676 ----------
3677 system : an instance of the `dlti` class or a tuple describing the system.
3678 The following gives the number of elements in the tuple and
3679 the interpretation:
3681 * 1 (instance of `dlti`)
3682 * 2 (numerator, denominator, dt)
3683 * 3 (zeros, poles, gain, dt)
3684 * 4 (A, B, C, D, dt)
3686 w : array_like, optional
3687 Array of frequencies (in radians/sample). Magnitude and phase data is
3688 calculated for every value in this array. If not given a reasonable
3689 set will be calculated.
3690 n : int, optional
3691 Number of frequency points to compute if `w` is not given. The `n`
3692 frequencies are logarithmically spaced in an interval chosen to
3693 include the influence of the poles and zeros of the system.
3694 whole : bool, optional
3695 Normally, if 'w' is not given, frequencies are computed from 0 to the
3696 Nyquist frequency, pi radians/sample (upper-half of unit-circle). If
3697 `whole` is True, compute frequencies from 0 to 2*pi radians/sample.
3699 Returns
3700 -------
3701 w : 1D ndarray
3702 Frequency array [radians/sample]
3703 H : 1D ndarray
3704 Array of complex magnitude values
3706 Notes
3707 -----
3708 If (num, den) is passed in for ``system``, coefficients for both the
3709 numerator and denominator should be specified in descending exponent
3710 order (e.g. ``z^2 + 3z + 5`` would be represented as ``[1, 3, 5]``).
3712 .. versionadded:: 0.18.0
3714 Examples
3715 --------
3716 Generating the Nyquist plot of a transfer function
3718 >>> from scipy import signal
3719 >>> import matplotlib.pyplot as plt
3721 Transfer function: H(z) = 1 / (z^2 + 2z + 3)
3723 >>> sys = signal.TransferFunction([1], [1, 2, 3], dt=0.05)
3725 >>> w, H = signal.dfreqresp(sys)
3727 >>> plt.figure()
3728 >>> plt.plot(H.real, H.imag, "b")
3729 >>> plt.plot(H.real, -H.imag, "r")
3730 >>> plt.show()
3732 """
3733 if not isinstance(system, dlti):
3734 if isinstance(system, lti):
3735 raise AttributeError('dfreqresp can only be used with '
3736 'discrete-time systems.')
3738 system = dlti(*system[:-1], dt=system[-1])
3740 if isinstance(system, StateSpace):
3741 # No SS->ZPK code exists right now, just SS->TF->ZPK
3742 system = system._as_tf()
3744 if not isinstance(system, (TransferFunction, ZerosPolesGain)):
3745 raise ValueError('Unknown system type')
3747 if system.inputs != 1 or system.outputs != 1:
3748 raise ValueError("dfreqresp requires a SISO (single input, single "
3749 "output) system.")
3751 if w is not None:
3752 worN = w
3753 else:
3754 worN = n
3756 if isinstance(system, TransferFunction):
3757 # Convert numerator and denominator from polynomials in the variable
3758 # 'z' to polynomials in the variable 'z^-1', as freqz expects.
3759 num, den = TransferFunction._z_to_zinv(system.num.ravel(), system.den)
3760 w, h = freqz(num, den, worN=worN, whole=whole)
3762 elif isinstance(system, ZerosPolesGain):
3763 w, h = freqz_zpk(system.zeros, system.poles, system.gain, worN=worN,
3764 whole=whole)
3766 return w, h
3769def dbode(system, w=None, n=100):
3770 """
3771 Calculate Bode magnitude and phase data of a discrete-time system.
3773 Parameters
3774 ----------
3775 system : an instance of the LTI class or a tuple describing the system.
3776 The following gives the number of elements in the tuple and
3777 the interpretation:
3779 * 1 (instance of `dlti`)
3780 * 2 (num, den, dt)
3781 * 3 (zeros, poles, gain, dt)
3782 * 4 (A, B, C, D, dt)
3784 w : array_like, optional
3785 Array of frequencies (in radians/sample). Magnitude and phase data is
3786 calculated for every value in this array. If not given a reasonable
3787 set will be calculated.
3788 n : int, optional
3789 Number of frequency points to compute if `w` is not given. The `n`
3790 frequencies are logarithmically spaced in an interval chosen to
3791 include the influence of the poles and zeros of the system.
3793 Returns
3794 -------
3795 w : 1D ndarray
3796 Frequency array [rad/time_unit]
3797 mag : 1D ndarray
3798 Magnitude array [dB]
3799 phase : 1D ndarray
3800 Phase array [deg]
3802 Notes
3803 -----
3804 If (num, den) is passed in for ``system``, coefficients for both the
3805 numerator and denominator should be specified in descending exponent
3806 order (e.g. ``z^2 + 3z + 5`` would be represented as ``[1, 3, 5]``).
3808 .. versionadded:: 0.18.0
3810 Examples
3811 --------
3812 >>> from scipy import signal
3813 >>> import matplotlib.pyplot as plt
3815 Transfer function: H(z) = 1 / (z^2 + 2z + 3)
3817 >>> sys = signal.TransferFunction([1], [1, 2, 3], dt=0.05)
3819 Equivalent: sys.bode()
3821 >>> w, mag, phase = signal.dbode(sys)
3823 >>> plt.figure()
3824 >>> plt.semilogx(w, mag) # Bode magnitude plot
3825 >>> plt.figure()
3826 >>> plt.semilogx(w, phase) # Bode phase plot
3827 >>> plt.show()
3829 """
3830 w, y = dfreqresp(system, w=w, n=n)
3832 if isinstance(system, dlti):
3833 dt = system.dt
3834 else:
3835 dt = system[-1]
3837 mag = 20.0 * numpy.log10(abs(y))
3838 phase = numpy.rad2deg(numpy.unwrap(numpy.angle(y)))
3840 return w / dt, mag, phase