Hide keyboard shortcuts

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 

21 

22import warnings 

23 

24# np.linalg.qr fails on some tests with LinAlgError: zgeqrf returns -7 

25# use scipy's qr until this is solved 

26 

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) 

34 

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 

40 

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'] 

45 

46 

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) 

55 

56 def __init__(self): 

57 """ 

58 Initialize the `lti` baseclass. 

59 

60 The heavy lifting is done by the subclasses. 

61 """ 

62 super(LinearTimeInvariant, self).__init__() 

63 

64 self.inputs = None 

65 self.outputs = None 

66 self._dt = None 

67 

68 @property 

69 def dt(self): 

70 """Return the sampling time of the system, `None` for `lti` systems.""" 

71 return self._dt 

72 

73 @property 

74 def _dt_dict(self): 

75 if self.dt is None: 

76 return {} 

77 else: 

78 return {'dt': self.dt} 

79 

80 @property 

81 def zeros(self): 

82 """Zeros of the system.""" 

83 return self.to_zpk().zeros 

84 

85 @property 

86 def poles(self): 

87 """Poles of the system.""" 

88 return self.to_zpk().poles 

89 

90 def _as_ss(self): 

91 """Convert to `StateSpace` system, without copying. 

92 

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() 

103 

104 def _as_zpk(self): 

105 """Convert to `ZerosPolesGain` system, without copying. 

106 

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() 

117 

118 def _as_tf(self): 

119 """Convert to `TransferFunction` system, without copying. 

120 

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() 

131 

132 

133class lti(LinearTimeInvariant): 

134 """ 

135 Continuous-time linear time invariant system base class. 

136 

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: 

143 

144 * 2: `TransferFunction`: (numerator, denominator) 

145 * 3: `ZerosPolesGain`: (zeros, poles, gain) 

146 * 4: `StateSpace`: (A, B, C, D) 

147 

148 Each argument can be an array or a sequence. 

149 

150 See Also 

151 -------- 

152 ZerosPolesGain, StateSpace, TransferFunction, dlti 

153 

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

159 

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]``). 

164 

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. 

170 

171 Examples 

172 -------- 

173 >>> from scipy import signal 

174 

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 ) 

183 

184 >>> signal.lti([1, 2], [3, 4], 5) 

185 ZerosPolesGainContinuous( 

186 array([1, 2]), 

187 array([3, 4]), 

188 5, 

189 dt: None 

190 ) 

191 

192 >>> signal.lti([3, 4], [1, 2]) 

193 TransferFunctionContinuous( 

194 array([3., 4.]), 

195 array([1., 2.]), 

196 dt: None 

197 ) 

198 

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) 

218 

219 def __init__(self, *system): 

220 """ 

221 Initialize the `lti` baseclass. 

222 

223 The heavy lifting is done by the subclasses. 

224 """ 

225 super(lti, self).__init__(*system) 

226 

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) 

233 

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) 

240 

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) 

247 

248 def bode(self, w=None, n=100): 

249 """ 

250 Calculate Bode magnitude and phase data of a continuous-time system. 

251 

252 Returns a 3-tuple containing arrays of frequencies [rad/s], magnitude 

253 [dB] and phase [deg]. See `bode` for details. 

254 

255 Examples 

256 -------- 

257 >>> from scipy import signal 

258 >>> import matplotlib.pyplot as plt 

259 

260 >>> sys = signal.TransferFunction([1], [1, 1]) 

261 >>> w, mag, phase = sys.bode() 

262 

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() 

268 

269 """ 

270 return bode(self, w=w, n=n) 

271 

272 def freqresp(self, w=None, n=10000): 

273 """ 

274 Calculate the frequency response of a continuous-time system. 

275 

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) 

281 

282 def to_discrete(self, dt, method='zoh', alpha=None): 

283 """Return a discretized version of the current system. 

284 

285 Parameters: See `cont2discrete` for details. 

286 

287 Returns 

288 ------- 

289 sys: instance of `dlti` 

290 """ 

291 raise NotImplementedError('to_discrete is not implemented for this ' 

292 'system class.') 

293 

294 

295class dlti(LinearTimeInvariant): 

296 """ 

297 Discrete-time linear time invariant system base class. 

298 

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: 

305 

306 * 2: `TransferFunction`: (numerator, denominator) 

307 * 3: `ZerosPolesGain`: (zeros, poles, gain) 

308 * 4: `StateSpace`: (A, B, C, D) 

309 

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

315 

316 See Also 

317 -------- 

318 ZerosPolesGain, StateSpace, TransferFunction, lti 

319 

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

325 

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. 

331 

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]``). 

336 

337 .. versionadded:: 0.18.0 

338 

339 Examples 

340 -------- 

341 >>> from scipy import signal 

342 

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 ) 

351 

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 ) 

360 

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 ) 

368 

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 ) 

375 

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) 

395 

396 def __init__(self, *system, **kwargs): 

397 """ 

398 Initialize the `lti` baseclass. 

399 

400 The heavy lifting is done by the subclasses. 

401 """ 

402 dt = kwargs.pop('dt', True) 

403 super(dlti, self).__init__(*system, **kwargs) 

404 

405 self.dt = dt 

406 

407 @property 

408 def dt(self): 

409 """Return the sampling time of the system.""" 

410 return self._dt 

411 

412 @dt.setter 

413 def dt(self, dt): 

414 self._dt = dt 

415 

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) 

422 

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) 

429 

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) 

436 

437 def bode(self, w=None, n=100): 

438 """ 

439 Calculate Bode magnitude and phase data of a discrete-time system. 

440 

441 Returns a 3-tuple containing arrays of frequencies [rad/s], magnitude 

442 [dB] and phase [deg]. See `dbode` for details. 

443 

444 Examples 

445 -------- 

446 >>> from scipy import signal 

447 >>> import matplotlib.pyplot as plt 

448 

449 Transfer function: H(z) = 1 / (z^2 + 2z + 3) with sampling time 0.5s 

450 

451 >>> sys = signal.TransferFunction([1], [1, 2, 3], dt=0.5) 

452 

453 Equivalent: signal.dbode(sys) 

454 

455 >>> w, mag, phase = sys.bode() 

456 

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() 

462 

463 """ 

464 return dbode(self, w=w, n=n) 

465 

466 def freqresp(self, w=None, n=10000, whole=False): 

467 """ 

468 Calculate the frequency response of a discrete-time system. 

469 

470 Returns a 2-tuple containing arrays of frequencies [rad/s] and 

471 complex magnitude. 

472 See `dfreqresp` for details. 

473 

474 """ 

475 return dfreqresp(self, w=w, n=n, whole=whole) 

476 

477 

478class TransferFunction(LinearTimeInvariant): 

479 r"""Linear Time Invariant system class in transfer function form. 

480 

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. 

490 

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: 

497 

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

505 

506 See Also 

507 -------- 

508 ZerosPolesGain, StateSpace, lti, dlti 

509 tf2ss, tf2zpk, tf2sos 

510 

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. 

519 

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]``) 

524 

525 Examples 

526 -------- 

527 Construct the transfer function: 

528 

529 .. math:: H(s) = \frac{s^2 + 3s + 3}{s^2 + 2s + 1} 

530 

531 >>> from scipy import signal 

532 

533 >>> num = [1, 3, 3] 

534 >>> den = [1, 2, 1] 

535 

536 >>> signal.TransferFunction(num, den) 

537 TransferFunctionContinuous( 

538 array([1., 3., 3.]), 

539 array([1., 2., 1.]), 

540 dt: None 

541 ) 

542 

543 Construct the transfer function with a sampling time of 0.1 seconds: 

544 

545 .. math:: H(z) = \frac{z^2 + 3z + 3}{z^2 + 2z + 1} 

546 

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 ) 

553 

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() 

559 

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) 

572 

573 # No special conversion needed 

574 return super(TransferFunction, cls).__new__(cls) 

575 

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 

581 

582 # Remove system arguments, not needed by parents anymore 

583 super(TransferFunction, self).__init__(**kwargs) 

584 

585 self._num = None 

586 self._den = None 

587 

588 self.num, self.den = normalize(*system) 

589 

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 ) 

598 

599 @property 

600 def num(self): 

601 """Numerator of the `TransferFunction` system.""" 

602 return self._num 

603 

604 @num.setter 

605 def num(self, num): 

606 self._num = atleast_1d(num) 

607 

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 

614 

615 @property 

616 def den(self): 

617 """Denominator of the `TransferFunction` system.""" 

618 return self._den 

619 

620 @den.setter 

621 def den(self, den): 

622 self._den = atleast_1d(den) 

623 

624 def _copy(self, system): 

625 """ 

626 Copy the parameters of another `TransferFunction` object 

627 

628 Parameters 

629 ---------- 

630 system : `TransferFunction` 

631 The `StateSpace` system that is to be copied 

632 

633 """ 

634 self.num = system.num 

635 self.den = system.den 

636 

637 def to_tf(self): 

638 """ 

639 Return a copy of the current `TransferFunction` system. 

640 

641 Returns 

642 ------- 

643 sys : instance of `TransferFunction` 

644 The current system (copy) 

645 

646 """ 

647 return copy.deepcopy(self) 

648 

649 def to_zpk(self): 

650 """ 

651 Convert system representation to `ZerosPolesGain`. 

652 

653 Returns 

654 ------- 

655 sys : instance of `ZerosPolesGain` 

656 Zeros, poles, gain representation of the current system 

657 

658 """ 

659 return ZerosPolesGain(*tf2zpk(self.num, self.den), 

660 **self._dt_dict) 

661 

662 def to_ss(self): 

663 """ 

664 Convert system representation to `StateSpace`. 

665 

666 Returns 

667 ------- 

668 sys : instance of `StateSpace` 

669 State space model of the current system 

670 

671 """ 

672 return StateSpace(*tf2ss(self.num, self.den), 

673 **self._dt_dict) 

674 

675 @staticmethod 

676 def _z_to_zinv(num, den): 

677 """Change a transfer function from the variable `z` to `z**-1`. 

678 

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]``. 

685 

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 

699 

700 @staticmethod 

701 def _zinv_to_z(num, den): 

702 """Change a transfer function from the variable `z` to `z**-1`. 

703 

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]``. 

710 

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 

724 

725 

726class TransferFunctionContinuous(TransferFunction, lti): 

727 r""" 

728 Continuous-time Linear Time Invariant system in transfer function form. 

729 

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. 

736 

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: 

743 

744 * 1: `lti` system: (`StateSpace`, `TransferFunction` or 

745 `ZerosPolesGain`) 

746 * 2: array_like: (numerator, denominator) 

747 

748 See Also 

749 -------- 

750 ZerosPolesGain, StateSpace, lti 

751 tf2ss, tf2zpk, tf2sos 

752 

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. 

761 

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]``) 

766 

767 Examples 

768 -------- 

769 Construct the transfer function: 

770 

771 .. math:: H(s) = \frac{s^2 + 3s + 3}{s^2 + 2s + 1} 

772 

773 >>> from scipy import signal 

774 

775 >>> num = [1, 3, 3] 

776 >>> den = [1, 2, 1] 

777 

778 >>> signal.TransferFunction(num, den) 

779 TransferFunctionContinuous( 

780 array([ 1., 3., 3.]), 

781 array([ 1., 2., 1.]), 

782 dt: None 

783 ) 

784 

785 """ 

786 def to_discrete(self, dt, method='zoh', alpha=None): 

787 """ 

788 Returns the discretized `TransferFunction` system. 

789 

790 Parameters: See `cont2discrete` for details. 

791 

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) 

801 

802 

803class TransferFunctionDiscrete(TransferFunction, dlti): 

804 r""" 

805 Discrete-time Linear Time Invariant system in transfer function form. 

806 

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. 

813 

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: 

820 

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

828 

829 See Also 

830 -------- 

831 ZerosPolesGain, StateSpace, dlti 

832 tf2ss, tf2zpk, tf2sos 

833 

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. 

840 

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]``). 

845 

846 Examples 

847 -------- 

848 Construct the transfer function with a sampling time of 0.5 seconds: 

849 

850 .. math:: H(z) = \frac{z^2 + 3z + 3}{z^2 + 2z + 1} 

851 

852 >>> from scipy import signal 

853 

854 >>> num = [1, 3, 3] 

855 >>> den = [1, 2, 1] 

856 

857 >>> signal.TransferFunction(num, den, 0.5) 

858 TransferFunctionDiscrete( 

859 array([ 1., 3., 3.]), 

860 array([ 1., 2., 1.]), 

861 dt: 0.5 

862 ) 

863 

864 """ 

865 pass 

866 

867 

868class ZerosPolesGain(LinearTimeInvariant): 

869 r""" 

870 Linear Time Invariant system class in zeros, poles, gain form. 

871 

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. 

878 

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: 

885 

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

893 

894 

895 See Also 

896 -------- 

897 TransferFunction, StateSpace, lti, dlti 

898 zpk2ss, zpk2tf, zpk2sos 

899 

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. 

908 

909 Examples 

910 -------- 

911 >>> from scipy import signal 

912 

913 Transfer function: H(s) = 5(s - 1)(s - 2) / (s - 3)(s - 4) 

914 

915 >>> signal.ZerosPolesGain([1, 2], [3, 4], 5) 

916 ZerosPolesGainContinuous( 

917 array([1, 2]), 

918 array([3, 4]), 

919 5, 

920 dt: None 

921 ) 

922 

923 Transfer function: H(z) = 5(z - 1)(z - 2) / (z - 3)(z - 4) 

924 

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 ) 

932 

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() 

938 

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 ) 

952 

953 # No special conversion needed 

954 return super(ZerosPolesGain, cls).__new__(cls) 

955 

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 

961 

962 super(ZerosPolesGain, self).__init__(**kwargs) 

963 

964 self._zeros = None 

965 self._poles = None 

966 self._gain = None 

967 

968 self.zeros, self.poles, self.gain = system 

969 

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 ) 

979 

980 @property 

981 def zeros(self): 

982 """Zeros of the `ZerosPolesGain` system.""" 

983 return self._zeros 

984 

985 @zeros.setter 

986 def zeros(self, zeros): 

987 self._zeros = atleast_1d(zeros) 

988 

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 

995 

996 @property 

997 def poles(self): 

998 """Poles of the `ZerosPolesGain` system.""" 

999 return self._poles 

1000 

1001 @poles.setter 

1002 def poles(self, poles): 

1003 self._poles = atleast_1d(poles) 

1004 

1005 @property 

1006 def gain(self): 

1007 """Gain of the `ZerosPolesGain` system.""" 

1008 return self._gain 

1009 

1010 @gain.setter 

1011 def gain(self, gain): 

1012 self._gain = gain 

1013 

1014 def _copy(self, system): 

1015 """ 

1016 Copy the parameters of another `ZerosPolesGain` system. 

1017 

1018 Parameters 

1019 ---------- 

1020 system : instance of `ZerosPolesGain` 

1021 The zeros, poles gain system that is to be copied 

1022 

1023 """ 

1024 self.poles = system.poles 

1025 self.zeros = system.zeros 

1026 self.gain = system.gain 

1027 

1028 def to_tf(self): 

1029 """ 

1030 Convert system representation to `TransferFunction`. 

1031 

1032 Returns 

1033 ------- 

1034 sys : instance of `TransferFunction` 

1035 Transfer function of the current system 

1036 

1037 """ 

1038 return TransferFunction(*zpk2tf(self.zeros, self.poles, self.gain), 

1039 **self._dt_dict) 

1040 

1041 def to_zpk(self): 

1042 """ 

1043 Return a copy of the current 'ZerosPolesGain' system. 

1044 

1045 Returns 

1046 ------- 

1047 sys : instance of `ZerosPolesGain` 

1048 The current system (copy) 

1049 

1050 """ 

1051 return copy.deepcopy(self) 

1052 

1053 def to_ss(self): 

1054 """ 

1055 Convert system representation to `StateSpace`. 

1056 

1057 Returns 

1058 ------- 

1059 sys : instance of `StateSpace` 

1060 State space model of the current system 

1061 

1062 """ 

1063 return StateSpace(*zpk2ss(self.zeros, self.poles, self.gain), 

1064 **self._dt_dict) 

1065 

1066 

1067class ZerosPolesGainContinuous(ZerosPolesGain, lti): 

1068 r""" 

1069 Continuous-time Linear Time Invariant system in zeros, poles, gain form. 

1070 

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. 

1076 

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: 

1083 

1084 * 1: `lti` system: (`StateSpace`, `TransferFunction` or 

1085 `ZerosPolesGain`) 

1086 * 3: array_like: (zeros, poles, gain) 

1087 

1088 See Also 

1089 -------- 

1090 TransferFunction, StateSpace, lti 

1091 zpk2ss, zpk2tf, zpk2sos 

1092 

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. 

1101 

1102 Examples 

1103 -------- 

1104 >>> from scipy import signal 

1105 

1106 Transfer function: H(s) = 5(s - 1)(s - 2) / (s - 3)(s - 4) 

1107 

1108 >>> signal.ZerosPolesGain([1, 2], [3, 4], 5) 

1109 ZerosPolesGainContinuous( 

1110 array([1, 2]), 

1111 array([3, 4]), 

1112 5, 

1113 dt: None 

1114 ) 

1115 

1116 """ 

1117 def to_discrete(self, dt, method='zoh', alpha=None): 

1118 """ 

1119 Returns the discretized `ZerosPolesGain` system. 

1120 

1121 Parameters: See `cont2discrete` for details. 

1122 

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) 

1133 

1134 

1135class ZerosPolesGainDiscrete(ZerosPolesGain, dlti): 

1136 r""" 

1137 Discrete-time Linear Time Invariant system in zeros, poles, gain form. 

1138 

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. 

1144 

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: 

1151 

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

1159 

1160 See Also 

1161 -------- 

1162 TransferFunction, StateSpace, dlti 

1163 zpk2ss, zpk2tf, zpk2sos 

1164 

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. 

1173 

1174 Examples 

1175 -------- 

1176 >>> from scipy import signal 

1177 

1178 Transfer function: H(s) = 5(s - 1)(s - 2) / (s - 3)(s - 4) 

1179 

1180 >>> signal.ZerosPolesGain([1, 2], [3, 4], 5) 

1181 ZerosPolesGainContinuous( 

1182 array([1, 2]), 

1183 array([3, 4]), 

1184 5, 

1185 dt: None 

1186 ) 

1187 

1188 Transfer function: H(z) = 5(z - 1)(z - 2) / (z - 3)(z - 4) 

1189 

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 ) 

1197 

1198 """ 

1199 pass 

1200 

1201 

1202def _atleast_2d_or_none(arg): 

1203 if arg is not None: 

1204 return atleast_2d(arg) 

1205 

1206 

1207class StateSpace(LinearTimeInvariant): 

1208 r""" 

1209 Linear Time Invariant system in state-space form. 

1210 

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. 

1216 

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: 

1223 

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

1231 

1232 See Also 

1233 -------- 

1234 TransferFunction, ZerosPolesGain, lti, dlti 

1235 ss2zpk, ss2tf, zpk2sos 

1236 

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. 

1244 

1245 Examples 

1246 -------- 

1247 >>> from scipy import signal 

1248 

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]]) 

1253 

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 ) 

1265 

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 ) 

1276 

1277 >>> a = np.array([[1, 0.1], [0, 1]]) 

1278 >>> b = np.array([[0.005], [0.1]]) 

1279 

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 ) 

1290 

1291 """ 

1292 

1293 # Override NumPy binary operations and ufuncs 

1294 __array_priority__ = 100.0 

1295 __array_ufunc__ = None 

1296 

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() 

1302 

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) 

1311 

1312 # No special conversion needed 

1313 return super(StateSpace, cls).__new__(cls) 

1314 

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 

1320 

1321 # Remove system arguments, not needed by parents anymore 

1322 super(StateSpace, self).__init__(**kwargs) 

1323 

1324 self._A = None 

1325 self._B = None 

1326 self._C = None 

1327 self._D = None 

1328 

1329 self.A, self.B, self.C, self.D = abcd_normalize(*system) 

1330 

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 ) 

1341 

1342 def _check_binop_other(self, other): 

1343 return isinstance(other, (StateSpace, np.ndarray, float, complex, 

1344 np.number, int)) 

1345 

1346 def __mul__(self, other): 

1347 """ 

1348 Post-multiply another system or a scalar 

1349 

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

1354 

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 

1363 

1364 if isinstance(other, StateSpace): 

1365 # Disallow mix of discrete and continuous systems. 

1366 if type(other) is not type(self): 

1367 return NotImplemented 

1368 

1369 if self.dt != other.dt: 

1370 raise TypeError('Cannot multiply systems with different `dt`.') 

1371 

1372 n1 = self.A.shape[0] 

1373 n2 = other.A.shape[0] 

1374 

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) 

1398 

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) 

1405 

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 

1410 

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) 

1416 

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) 

1423 

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) 

1427 

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 

1434 

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

1440 

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") 

1469 

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) 

1476 

1477 def __sub__(self, other): 

1478 if not self._check_binop_other(other): 

1479 return NotImplemented 

1480 

1481 return self.__add__(-other) 

1482 

1483 def __radd__(self, other): 

1484 if not self._check_binop_other(other): 

1485 return NotImplemented 

1486 

1487 return self.__add__(other) 

1488 

1489 def __rsub__(self, other): 

1490 if not self._check_binop_other(other): 

1491 return NotImplemented 

1492 

1493 return (-self).__add__(other) 

1494 

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 

1502 

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") 

1506 

1507 return self.__mul__(1/other) 

1508 

1509 @property 

1510 def A(self): 

1511 """State matrix of the `StateSpace` system.""" 

1512 return self._A 

1513 

1514 @A.setter 

1515 def A(self, A): 

1516 self._A = _atleast_2d_or_none(A) 

1517 

1518 @property 

1519 def B(self): 

1520 """Input matrix of the `StateSpace` system.""" 

1521 return self._B 

1522 

1523 @B.setter 

1524 def B(self, B): 

1525 self._B = _atleast_2d_or_none(B) 

1526 self.inputs = self.B.shape[-1] 

1527 

1528 @property 

1529 def C(self): 

1530 """Output matrix of the `StateSpace` system.""" 

1531 return self._C 

1532 

1533 @C.setter 

1534 def C(self, C): 

1535 self._C = _atleast_2d_or_none(C) 

1536 self.outputs = self.C.shape[0] 

1537 

1538 @property 

1539 def D(self): 

1540 """Feedthrough matrix of the `StateSpace` system.""" 

1541 return self._D 

1542 

1543 @D.setter 

1544 def D(self, D): 

1545 self._D = _atleast_2d_or_none(D) 

1546 

1547 def _copy(self, system): 

1548 """ 

1549 Copy the parameters of another `StateSpace` system. 

1550 

1551 Parameters 

1552 ---------- 

1553 system : instance of `StateSpace` 

1554 The state-space system that is to be copied 

1555 

1556 """ 

1557 self.A = system.A 

1558 self.B = system.B 

1559 self.C = system.C 

1560 self.D = system.D 

1561 

1562 def to_tf(self, **kwargs): 

1563 """ 

1564 Convert system representation to `TransferFunction`. 

1565 

1566 Parameters 

1567 ---------- 

1568 kwargs : dict, optional 

1569 Additional keywords passed to `ss2zpk` 

1570 

1571 Returns 

1572 ------- 

1573 sys : instance of `TransferFunction` 

1574 Transfer function of the current system 

1575 

1576 """ 

1577 return TransferFunction(*ss2tf(self._A, self._B, self._C, self._D, 

1578 **kwargs), **self._dt_dict) 

1579 

1580 def to_zpk(self, **kwargs): 

1581 """ 

1582 Convert system representation to `ZerosPolesGain`. 

1583 

1584 Parameters 

1585 ---------- 

1586 kwargs : dict, optional 

1587 Additional keywords passed to `ss2zpk` 

1588 

1589 Returns 

1590 ------- 

1591 sys : instance of `ZerosPolesGain` 

1592 Zeros, poles, gain representation of the current system 

1593 

1594 """ 

1595 return ZerosPolesGain(*ss2zpk(self._A, self._B, self._C, self._D, 

1596 **kwargs), **self._dt_dict) 

1597 

1598 def to_ss(self): 

1599 """ 

1600 Return a copy of the current `StateSpace` system. 

1601 

1602 Returns 

1603 ------- 

1604 sys : instance of `StateSpace` 

1605 The current system (copy) 

1606 

1607 """ 

1608 return copy.deepcopy(self) 

1609 

1610 

1611class StateSpaceContinuous(StateSpace, lti): 

1612 r""" 

1613 Continuous-time Linear Time Invariant system in state-space form. 

1614 

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. 

1619 

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: 

1626 

1627 * 1: `lti` system: (`StateSpace`, `TransferFunction` or 

1628 `ZerosPolesGain`) 

1629 * 4: array_like: (A, B, C, D) 

1630 

1631 See Also 

1632 -------- 

1633 TransferFunction, ZerosPolesGain, lti 

1634 ss2zpk, ss2tf, zpk2sos 

1635 

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. 

1643 

1644 Examples 

1645 -------- 

1646 >>> from scipy import signal 

1647 

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]]) 

1652 

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 ) 

1664 

1665 """ 

1666 def to_discrete(self, dt, method='zoh', alpha=None): 

1667 """ 

1668 Returns the discretized `StateSpace` system. 

1669 

1670 Parameters: See `cont2discrete` for details. 

1671 

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) 

1681 

1682 

1683class StateSpaceDiscrete(StateSpace, dlti): 

1684 r""" 

1685 Discrete-time Linear Time Invariant system in state-space form. 

1686 

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. 

1691 

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: 

1698 

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

1706 

1707 See Also 

1708 -------- 

1709 TransferFunction, ZerosPolesGain, dlti 

1710 ss2zpk, ss2tf, zpk2sos 

1711 

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. 

1719 

1720 Examples 

1721 -------- 

1722 >>> from scipy import signal 

1723 

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]]) 

1728 

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 ) 

1739 

1740 """ 

1741 pass 

1742 

1743 

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

1748 

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: 

1754 

1755 * 1: (instance of `lti`) 

1756 * 2: (num, den) 

1757 * 3: (zeros, poles, gain) 

1758 * 4: (A, B, C, D) 

1759 

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. 

1776 

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. 

1785 

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. 

1792 

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]``). 

1796 

1797 See Also 

1798 -------- 

1799 lsim 

1800 

1801 Examples 

1802 -------- 

1803 We'll use `lsim2` to simulate an analog Bessel filter applied to 

1804 a signal. 

1805 

1806 >>> from scipy.signal import bessel, lsim2 

1807 >>> import matplotlib.pyplot as plt 

1808 

1809 Create a low-pass Bessel filter with a cutoff of 12 Hz. 

1810 

1811 >>> b, a = bessel(N=5, Wn=2*np.pi*12, btype='lowpass', analog=True) 

1812 

1813 Generate data to which the filter is applied. 

1814 

1815 >>> t = np.linspace(0, 1.25, 500, endpoint=False) 

1816 

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. 

1820 

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

1823 

1824 Simulate the filter with `lsim2`. 

1825 

1826 >>> tout, yout, xout = lsim2((b, a), U=u, T=t) 

1827 

1828 Plot the result. 

1829 

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() 

1836 

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. 

1840 

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) 

1847 

1848 `t` and `u` define the time and input signal for the system to 

1849 be simulated. 

1850 

1851 >>> t = np.linspace(0, 5, num=50) 

1852 >>> u = np.ones_like(t) 

1853 

1854 Compute the simulation, and then plot `y`. As expected, the plot shows 

1855 the curve ``y = 0.5*t**2``. 

1856 

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() 

1862 

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() 

1871 

1872 if X0 is None: 

1873 X0 = zeros(sys.B.shape[0], sys.A.dtype) 

1874 

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) 

1883 

1884 T = atleast_1d(T) 

1885 if len(T.shape) != 1: 

1886 raise ValueError("T must be a rank-1 array.") 

1887 

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.") 

1896 

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) 

1905 

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

1917 

1918 return T, squeeze(transpose(yout)), xout 

1919 

1920 

1921def _cast_to_array_dtype(in1, in2): 

1922 """Cast array to dtype of other array, while avoiding ComplexWarning. 

1923 

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) 

1931 

1932 return in1 

1933 

1934 

1935def lsim(system, U, T, X0=None, interp=True): 

1936 """ 

1937 Simulate output of a continuous-time linear system. 

1938 

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: 

1944 

1945 * 1: (instance of `lti`) 

1946 * 2: (num, den) 

1947 * 3: (zeros, poles, gain) 

1948 * 4: (A, B, C, D) 

1949 

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. 

1963 

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. 

1972 

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]``). 

1978 

1979 Examples 

1980 -------- 

1981 We'll use `lsim` to simulate an analog Bessel filter applied to 

1982 a signal. 

1983 

1984 >>> from scipy.signal import bessel, lsim 

1985 >>> import matplotlib.pyplot as plt 

1986 

1987 Create a low-pass Bessel filter with a cutoff of 12 Hz. 

1988 

1989 >>> b, a = bessel(N=5, Wn=2*np.pi*12, btype='lowpass', analog=True) 

1990 

1991 Generate data to which the filter is applied. 

1992 

1993 >>> t = np.linspace(0, 1.25, 500, endpoint=False) 

1994 

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. 

1998 

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

2001 

2002 Simulate the filter with `lsim`. 

2003 

2004 >>> tout, yout, xout = lsim((b, a), U=u, T=t) 

2005 

2006 Plot the result. 

2007 

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() 

2014 

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. 

2018 

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) 

2025 

2026 `t` and `u` define the time and input signal for the system to 

2027 be simulated. 

2028 

2029 >>> t = np.linspace(0, 5, num=50) 

2030 >>> u = np.ones_like(t) 

2031 

2032 Compute the simulation, and then plot `y`. As expected, the plot shows 

2033 the curve ``y = 0.5*t**2``. 

2034 

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() 

2040 

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.") 

2052 

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] 

2056 

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) 

2061 

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") 

2069 

2070 no_input = (U is None or 

2071 (isinstance(U, (int, float)) and U == 0.) or 

2072 not np.any(U)) 

2073 

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) 

2079 

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) 

2085 

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) 

2094 

2095 # Nonzero input 

2096 U = atleast_1d(U) 

2097 if U.ndim == 1: 

2098 U = U[:, np.newaxis] 

2099 

2100 if U.shape[0] != n_steps: 

2101 raise ValueError("U must have the same number of rows " 

2102 "as elements in T.") 

2103 

2104 if U.shape[1] != n_inputs: 

2105 raise ValueError("System does not define that many inputs.") 

2106 

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

2146 

2147 yout = (squeeze(dot(xout, transpose(C))) + squeeze(dot(U, transpose(D)))) 

2148 return T, squeeze(yout), squeeze(xout) 

2149 

2150 

2151def _default_response_times(A, n): 

2152 """Compute a reasonable set of time samples for the response time. 

2153 

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. 

2157 

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. 

2164 

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 

2181 

2182 

2183def impulse(system, X0=None, T=None, N=None): 

2184 """Impulse response of continuous-time system. 

2185 

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: 

2192 

2193 * 1 (instance of `lti`) 

2194 * 2 (num, den) 

2195 * 3 (zeros, poles, gain) 

2196 * 4 (A, B, C, D) 

2197 

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

2204 

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

2212 

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]``). 

2218 

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)`` 

2223 

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) 

2229 

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) 

2248 

2249 _, h, _ = lsim(sys, 0., T, X, interp=False) 

2250 return T, h 

2251 

2252 

2253def impulse2(system, X0=None, T=None, N=None, **kwargs): 

2254 """ 

2255 Impulse response of a single-input, continuous-time linear system. 

2256 

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: 

2263 

2264 * 1 (instance of `lti`) 

2265 * 2 (num, den) 

2266 * 3 (zeros, poles, gain) 

2267 * 4 (A, B, C, D) 

2268 

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. 

2283 

2284 Returns 

2285 ------- 

2286 T : ndarray 

2287 The time values for the output. 

2288 yout : ndarray 

2289 The output response of the system. 

2290 

2291 See Also 

2292 -------- 

2293 impulse, lsim2, scipy.integrate.odeint 

2294 

2295 Notes 

2296 ----- 

2297 The solution is generated by calling `scipy.signal.lsim2`, which uses 

2298 the differential equation solver `scipy.integrate.odeint`. 

2299 

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]``). 

2303 

2304 .. versionadded:: 0.8.0 

2305 

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)`` 

2310 

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) 

2316 

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) 

2335 

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 

2341 

2342 

2343def step(system, X0=None, T=None, N=None): 

2344 """Step response of continuous-time system. 

2345 

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: 

2352 

2353 * 1 (instance of `lti`) 

2354 * 2 (num, den) 

2355 * 3 (zeros, poles, gain) 

2356 * 4 (A, B, C, D) 

2357 

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. 

2364 

2365 Returns 

2366 ------- 

2367 T : 1D ndarray 

2368 Output time points. 

2369 yout : 1D ndarray 

2370 Step response of system. 

2371 

2372 See also 

2373 -------- 

2374 scipy.signal.step2 

2375 

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]``). 

2381 

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() 

2393 

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] 

2411 

2412 

2413def step2(system, X0=None, T=None, N=None, **kwargs): 

2414 """Step response of continuous-time system. 

2415 

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. 

2419 

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: 

2426 

2427 * 1 (instance of `lti`) 

2428 * 2 (num, den) 

2429 * 3 (zeros, poles, gain) 

2430 * 4 (A, B, C, D) 

2431 

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. 

2443 

2444 Returns 

2445 ------- 

2446 T : 1D ndarray 

2447 Output time points. 

2448 yout : 1D ndarray 

2449 Step response of system. 

2450 

2451 See also 

2452 -------- 

2453 scipy.signal.step 

2454 

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]``). 

2460 

2461 .. versionadded:: 0.8.0 

2462 

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() 

2474 

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] 

2492 

2493 

2494def bode(system, w=None, n=100): 

2495 """ 

2496 Calculate Bode magnitude and phase data of a continuous-time system. 

2497 

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: 

2503 

2504 * 1 (instance of `lti`) 

2505 * 2 (num, den) 

2506 * 3 (zeros, poles, gain) 

2507 * 4 (A, B, C, D) 

2508 

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. 

2517 

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] 

2526 

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]``). 

2532 

2533 .. versionadded:: 0.11.0 

2534 

2535 Examples 

2536 -------- 

2537 >>> from scipy import signal 

2538 >>> import matplotlib.pyplot as plt 

2539 

2540 >>> sys = signal.TransferFunction([1], [1, 1]) 

2541 >>> w, mag, phase = signal.bode(sys) 

2542 

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() 

2548 

2549 """ 

2550 w, y = freqresp(system, w=w, n=n) 

2551 

2552 mag = 20.0 * numpy.log10(abs(y)) 

2553 phase = numpy.unwrap(numpy.arctan2(y.imag, y.real)) * 180.0 / numpy.pi 

2554 

2555 return w, mag, phase 

2556 

2557 

2558def freqresp(system, w=None, n=10000): 

2559 """Calculate the frequency response of a continuous-time system. 

2560 

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: 

2566 

2567 * 1 (instance of `lti`) 

2568 * 2 (num, den) 

2569 * 3 (zeros, poles, gain) 

2570 * 4 (A, B, C, D) 

2571 

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. 

2580 

2581 Returns 

2582 ------- 

2583 w : 1D ndarray 

2584 Frequency array [rad/s] 

2585 H : 1D ndarray 

2586 Array of complex magnitude values 

2587 

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]``). 

2593 

2594 Examples 

2595 -------- 

2596 Generating the Nyquist plot of a transfer function 

2597 

2598 >>> from scipy import signal 

2599 >>> import matplotlib.pyplot as plt 

2600 

2601 Transfer function: H(s) = 5 / (s-1)^3 

2602 

2603 >>> s1 = signal.ZerosPolesGain([], [1, 1, 1], [5]) 

2604 

2605 >>> w, H = signal.freqresp(s1) 

2606 

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() 

2622 

2623 if sys.inputs != 1 or sys.outputs != 1: 

2624 raise ValueError("freqresp() requires a SISO (single input, single " 

2625 "output) system.") 

2626 

2627 if w is not None: 

2628 worN = w 

2629 else: 

2630 worN = n 

2631 

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) 

2636 

2637 elif isinstance(sys, ZerosPolesGain): 

2638 w, h = freqs_zpk(sys.zeros, sys.poles, sys.gain, worN=worN) 

2639 

2640 return w, h 

2641 

2642 

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) 

2648 

2649 

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 

2656 

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'") 

2684 

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") 

2689 

2690 if maxiter < 1: 

2691 raise ValueError("maxiter must be at least equal to 1") 

2692 

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") 

2697 

2698 return update_loop, poles 

2699 

2700 

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

2713 

2714 ordered_poles = np.hstack((ordered_poles, im_poles)) 

2715 

2716 if poles.shape[0] != len(ordered_poles): 

2717 raise ValueError("Complex poles must come with their conjugates") 

2718 return ordered_poles 

2719 

2720 

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 

2728 

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 ! 

2735 

2736 # After merge of gh-4249 great speed improvements could be achieved 

2737 # using QR updates instead of full QR in the line below 

2738 

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") 

2742 

2743 mat_ker_pj = np.dot(ker_pole[j], ker_pole[j].T) 

2744 yj = np.dot(mat_ker_pj, Q[:, -1]) 

2745 

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 ?) 

2751 

2752 if not np.allclose(yj, 0): 

2753 xj = yj/np.linalg.norm(yj) 

2754 transfer_matrix[:, j] = xj 

2755 

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) 

2760 

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 

2766 

2767 

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] 

2775 

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]) 

2779 

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] 

2786 

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])) 

2792 

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 ] 

2831 

2832 

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 

2841 

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) 

2846 

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] 

2853 

2854 # what follows is a rough python translation of the formulas 

2855 # in section 6.2 page 20 (step 4) 

2856 

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) 

2872 

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]) 

2882 

2883 

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 

2890 

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 

2898 

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 = [[],[]] 

2908 

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) 

2964 

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 

2980 

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") 

2984 

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) 

2993 

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 

3005 

3006 

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) 

3020 

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 

3028 

3029 nb_try += 1 

3030 return stop, cur_rtol, nb_try 

3031 

3032 

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. 

3036 

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. 

3040 

3041 SISO, MISO and MIMO systems are supported. 

3042 

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: 

3052 

3053 - 'YT': Yang Tits 

3054 - 'KNV0': Kautsky, Nichols, Van Dooren update method 0 

3055 

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. 

3065 

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

3093 

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'``. 

3103 

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. 

3110 

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. 

3119 

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. 

3124 

3125 [2]_ is available as a technical report on the following URL: 

3126 https://hdl.handle.net/1903/5598 

3127 

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. 

3136 

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]_): 

3142 

3143 >>> from scipy import signal 

3144 >>> import matplotlib.pyplot as plt 

3145 

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]) 

3155 

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. 

3159 

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]]) 

3164 

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 ]) 

3168 

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]]) 

3175 

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: 

3179 

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 

3184 

3185 Now a simple example for complex poles: 

3186 

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') 

3197 

3198 We can plot the desired and computed poles in the complex plane: 

3199 

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) 

3210 

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) 

3214 

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 

3219 

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, :] 

3228 

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 = [] 

3270 

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 

3280 

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 

3286 

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") 

3290 

3291 ker_pole_j = Q[:, pole_space_j.shape[1]:] 

3292 

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. 

3308 

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]) 

3316 

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) 

3321 

3322 if j == 0: 

3323 transfer_matrix = transfer_matrix_j 

3324 else: 

3325 transfer_matrix = np.hstack((transfer_matrix, transfer_matrix_j)) 

3326 

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) 

3339 

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 

3356 

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") 

3365 

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) 

3370 

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 

3380 

3381 return full_state_feedback 

3382 

3383 

3384def dlsim(system, u, t=None, x0=None): 

3385 """ 

3386 Simulate output of a discrete-time linear system. 

3387 

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: 

3394 

3395 * 1: (instance of `dlti`) 

3396 * 3: (num, den, dt) 

3397 * 4: (zeros, poles, gain, dt) 

3398 * 5: (A, B, C, D, dt) 

3399 

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

3410 

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. 

3420 

3421 See Also 

3422 -------- 

3423 lsim, dstep, dimpulse, cont2discrete 

3424 

3425 Examples 

3426 -------- 

3427 A simple integrator transfer function with a discrete time step of 1.0 

3428 could be implemented as: 

3429 

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.]]) 

3437 

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]) 

3445 

3446 # Condition needed to ensure output remains compatible 

3447 is_ss_input = isinstance(system, StateSpace) 

3448 system = system._as_ss() 

3449 

3450 u = np.atleast_1d(u) 

3451 

3452 if u.ndim == 1: 

3453 u = np.atleast_2d(u).T 

3454 

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 

3461 

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) 

3466 

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) 

3472 

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] 

3479 

3480 u_dt_interp = interp1d(t, u.transpose(), copy=False, bounds_error=True) 

3481 u_dt = u_dt_interp(tout).transpose() 

3482 

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, :])) 

3489 

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, :])) 

3493 

3494 if is_ss_input: 

3495 return tout, yout, xout 

3496 else: 

3497 return tout, yout 

3498 

3499 

3500def dimpulse(system, x0=None, t=None, n=None): 

3501 """ 

3502 Impulse response of discrete-time system. 

3503 

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: 

3510 

3511 * 1: (instance of `dlti`) 

3512 * 3: (num, den, dt) 

3513 * 4: (zeros, poles, gain, dt) 

3514 * 5: (A, B, C, D, dt) 

3515 

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

3522 

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. 

3530 

3531 See Also 

3532 -------- 

3533 impulse, dstep, dlsim, cont2discrete 

3534 

3535 Examples 

3536 -------- 

3537 >>> from scipy import signal 

3538 >>> import matplotlib.pyplot as plt 

3539 

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') 

3546 

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() 

3556 

3557 # Default to 100 samples if unspecified 

3558 if n is None: 

3559 n = 100 

3560 

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) 

3567 

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 

3573 

3574 one_output = dlsim(system, u, t=t, x0=x0) 

3575 

3576 if yout is None: 

3577 yout = (one_output[1],) 

3578 else: 

3579 yout = yout + (one_output[1],) 

3580 

3581 tout = one_output[0] 

3582 

3583 return tout, yout 

3584 

3585 

3586def dstep(system, x0=None, t=None, n=None): 

3587 """ 

3588 Step response of discrete-time system. 

3589 

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: 

3596 

3597 * 1: (instance of `dlti`) 

3598 * 3: (num, den, dt) 

3599 * 4: (zeros, poles, gain, dt) 

3600 * 5: (A, B, C, D, dt) 

3601 

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

3608 

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. 

3616 

3617 See Also 

3618 -------- 

3619 step, dimpulse, dlsim, cont2discrete 

3620 

3621 Examples 

3622 -------- 

3623 >>> from scipy import signal 

3624 >>> import matplotlib.pyplot as plt 

3625 

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() 

3641 

3642 # Default to 100 samples if unspecified 

3643 if n is None: 

3644 n = 100 

3645 

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) 

3652 

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],)) 

3658 

3659 one_output = dlsim(system, u, t=t, x0=x0) 

3660 

3661 if yout is None: 

3662 yout = (one_output[1],) 

3663 else: 

3664 yout = yout + (one_output[1],) 

3665 

3666 tout = one_output[0] 

3667 

3668 return tout, yout 

3669 

3670 

3671def dfreqresp(system, w=None, n=10000, whole=False): 

3672 """ 

3673 Calculate the frequency response of a discrete-time system. 

3674 

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: 

3680 

3681 * 1 (instance of `dlti`) 

3682 * 2 (numerator, denominator, dt) 

3683 * 3 (zeros, poles, gain, dt) 

3684 * 4 (A, B, C, D, dt) 

3685 

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. 

3698 

3699 Returns 

3700 ------- 

3701 w : 1D ndarray 

3702 Frequency array [radians/sample] 

3703 H : 1D ndarray 

3704 Array of complex magnitude values 

3705 

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]``). 

3711 

3712 .. versionadded:: 0.18.0 

3713 

3714 Examples 

3715 -------- 

3716 Generating the Nyquist plot of a transfer function 

3717 

3718 >>> from scipy import signal 

3719 >>> import matplotlib.pyplot as plt 

3720 

3721 Transfer function: H(z) = 1 / (z^2 + 2z + 3) 

3722 

3723 >>> sys = signal.TransferFunction([1], [1, 2, 3], dt=0.05) 

3724 

3725 >>> w, H = signal.dfreqresp(sys) 

3726 

3727 >>> plt.figure() 

3728 >>> plt.plot(H.real, H.imag, "b") 

3729 >>> plt.plot(H.real, -H.imag, "r") 

3730 >>> plt.show() 

3731 

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.') 

3737 

3738 system = dlti(*system[:-1], dt=system[-1]) 

3739 

3740 if isinstance(system, StateSpace): 

3741 # No SS->ZPK code exists right now, just SS->TF->ZPK 

3742 system = system._as_tf() 

3743 

3744 if not isinstance(system, (TransferFunction, ZerosPolesGain)): 

3745 raise ValueError('Unknown system type') 

3746 

3747 if system.inputs != 1 or system.outputs != 1: 

3748 raise ValueError("dfreqresp requires a SISO (single input, single " 

3749 "output) system.") 

3750 

3751 if w is not None: 

3752 worN = w 

3753 else: 

3754 worN = n 

3755 

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) 

3761 

3762 elif isinstance(system, ZerosPolesGain): 

3763 w, h = freqz_zpk(system.zeros, system.poles, system.gain, worN=worN, 

3764 whole=whole) 

3765 

3766 return w, h 

3767 

3768 

3769def dbode(system, w=None, n=100): 

3770 """ 

3771 Calculate Bode magnitude and phase data of a discrete-time system. 

3772 

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: 

3778 

3779 * 1 (instance of `dlti`) 

3780 * 2 (num, den, dt) 

3781 * 3 (zeros, poles, gain, dt) 

3782 * 4 (A, B, C, D, dt) 

3783 

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. 

3792 

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] 

3801 

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]``). 

3807 

3808 .. versionadded:: 0.18.0 

3809 

3810 Examples 

3811 -------- 

3812 >>> from scipy import signal 

3813 >>> import matplotlib.pyplot as plt 

3814 

3815 Transfer function: H(z) = 1 / (z^2 + 2z + 3) 

3816 

3817 >>> sys = signal.TransferFunction([1], [1, 2, 3], dt=0.05) 

3818 

3819 Equivalent: sys.bode() 

3820 

3821 >>> w, mag, phase = signal.dbode(sys) 

3822 

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() 

3828 

3829 """ 

3830 w, y = dfreqresp(system, w=w, n=n) 

3831 

3832 if isinstance(system, dlti): 

3833 dt = system.dt 

3834 else: 

3835 dt = system[-1] 

3836 

3837 mag = 20.0 * numpy.log10(abs(y)) 

3838 phase = numpy.rad2deg(numpy.unwrap(numpy.angle(y))) 

3839 

3840 return w / dt, mag, phase