_ltisys.py 118 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550
  1. """
  2. ltisys -- a collection of classes and functions for modeling linear
  3. time 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. import warnings
  22. from types import GenericAlias
  23. # np.linalg.qr fails on some tests with LinAlgError: zgeqrf returns -7
  24. # use scipy's qr until this is solved
  25. from scipy.linalg import qr as s_qr
  26. from scipy import linalg
  27. from scipy.interpolate import make_interp_spline
  28. from ._filter_design import (tf2zpk, zpk2tf, normalize, freqs, freqz, freqs_zpk,
  29. freqz_zpk)
  30. from ._lti_conversion import (tf2ss, abcd_normalize, ss2tf, zpk2ss, ss2zpk,
  31. cont2discrete)
  32. import numpy as np
  33. from numpy import (real, atleast_1d, squeeze, asarray, zeros,
  34. dot, transpose, ones, linspace)
  35. import copy
  36. __all__ = ['lti', 'dlti', 'TransferFunction', 'ZerosPolesGain', 'StateSpace',
  37. 'lsim', 'impulse', 'step', 'bode',
  38. 'freqresp', 'place_poles', 'dlsim', 'dstep', 'dimpulse',
  39. 'dfreqresp', 'dbode']
  40. class LinearTimeInvariant:
  41. # generic type compatibility with scipy-stubs
  42. __class_getitem__ = classmethod(GenericAlias)
  43. def __new__(cls, *system, **kwargs):
  44. """Create a new object, don't allow direct instances."""
  45. if cls is LinearTimeInvariant:
  46. raise NotImplementedError('The LinearTimeInvariant class is not '
  47. 'meant to be used directly, use `lti` '
  48. 'or `dlti` instead.')
  49. return super().__new__(cls)
  50. def __init__(self):
  51. """
  52. Initialize the `lti` baseclass.
  53. The heavy lifting is done by the subclasses.
  54. """
  55. super().__init__()
  56. self.inputs = None
  57. self.outputs = None
  58. self._dt = None
  59. @property
  60. def dt(self):
  61. """Return the sampling time of the system, `None` for `lti` systems."""
  62. return self._dt
  63. @property
  64. def _dt_dict(self):
  65. if self.dt is None:
  66. return {}
  67. else:
  68. return {'dt': self.dt}
  69. @property
  70. def zeros(self):
  71. """Zeros of the system."""
  72. return self.to_zpk().zeros
  73. @property
  74. def poles(self):
  75. """Poles of the system."""
  76. return self.to_zpk().poles
  77. def _as_ss(self):
  78. """Convert to `StateSpace` system, without copying.
  79. Returns
  80. -------
  81. sys: StateSpace
  82. The `StateSpace` system. If the class is already an instance of
  83. `StateSpace` then this instance is returned.
  84. """
  85. if isinstance(self, StateSpace):
  86. return self
  87. else:
  88. return self.to_ss()
  89. def _as_zpk(self):
  90. """Convert to `ZerosPolesGain` system, without copying.
  91. Returns
  92. -------
  93. sys: ZerosPolesGain
  94. The `ZerosPolesGain` system. If the class is already an instance of
  95. `ZerosPolesGain` then this instance is returned.
  96. """
  97. if isinstance(self, ZerosPolesGain):
  98. return self
  99. else:
  100. return self.to_zpk()
  101. def _as_tf(self):
  102. """Convert to `TransferFunction` system, without copying.
  103. Returns
  104. -------
  105. sys: ZerosPolesGain
  106. The `TransferFunction` system. If the class is already an instance of
  107. `TransferFunction` then this instance is returned.
  108. """
  109. if isinstance(self, TransferFunction):
  110. return self
  111. else:
  112. return self.to_tf()
  113. class lti(LinearTimeInvariant):
  114. r"""
  115. Continuous-time linear time invariant system base class.
  116. Parameters
  117. ----------
  118. *system : arguments
  119. The `lti` class can be instantiated with either 2, 3 or 4 arguments.
  120. The following gives the number of arguments and the corresponding
  121. continuous-time subclass that is created:
  122. * 2: `TransferFunction`: (numerator, denominator)
  123. * 3: `ZerosPolesGain`: (zeros, poles, gain)
  124. * 4: `StateSpace`: (A, B, C, D)
  125. Each argument can be an array or a sequence.
  126. See Also
  127. --------
  128. ZerosPolesGain, StateSpace, TransferFunction, dlti
  129. Notes
  130. -----
  131. `lti` instances do not exist directly. Instead, `lti` creates an instance
  132. of one of its subclasses: `StateSpace`, `TransferFunction` or
  133. `ZerosPolesGain`.
  134. If (numerator, denominator) is passed in for ``*system``, coefficients for
  135. both the numerator and denominator should be specified in descending
  136. exponent order (e.g., ``s^2 + 3s + 5`` would be represented as ``[1, 3,
  137. 5]``).
  138. Changing the value of properties that are not directly part of the current
  139. system representation (such as the `zeros` of a `StateSpace` system) is
  140. very inefficient and may lead to numerical inaccuracies. It is better to
  141. convert to the specific system representation first. For example, call
  142. ``sys = sys.to_zpk()`` before accessing/changing the zeros, poles or gain.
  143. Examples
  144. --------
  145. >>> from scipy import signal
  146. >>> signal.lti(1, 2, 3, 4)
  147. StateSpaceContinuous(
  148. array([[1]]),
  149. array([[2]]),
  150. array([[3]]),
  151. array([[4]]),
  152. dt: None
  153. )
  154. Construct the transfer function
  155. :math:`H(s) = \frac{5(s - 1)(s - 2)}{(s - 3)(s - 4)}`:
  156. >>> signal.lti([1, 2], [3, 4], 5)
  157. ZerosPolesGainContinuous(
  158. array([1, 2]),
  159. array([3, 4]),
  160. 5,
  161. dt: None
  162. )
  163. Construct the transfer function :math:`H(s) = \frac{3s + 4}{1s + 2}`:
  164. >>> signal.lti([3, 4], [1, 2])
  165. TransferFunctionContinuous(
  166. array([3., 4.]),
  167. array([1., 2.]),
  168. dt: None
  169. )
  170. """
  171. def __new__(cls, *system):
  172. """Create an instance of the appropriate subclass."""
  173. if cls is lti:
  174. N = len(system)
  175. if N == 2:
  176. return TransferFunctionContinuous.__new__(
  177. TransferFunctionContinuous, *system)
  178. elif N == 3:
  179. return ZerosPolesGainContinuous.__new__(
  180. ZerosPolesGainContinuous, *system)
  181. elif N == 4:
  182. return StateSpaceContinuous.__new__(StateSpaceContinuous,
  183. *system)
  184. else:
  185. raise ValueError("`system` needs to be an instance of `lti` "
  186. "or have 2, 3 or 4 arguments.")
  187. # __new__ was called from a subclass, let it call its own functions
  188. return super().__new__(cls)
  189. def __init__(self, *system):
  190. """
  191. Initialize the `lti` baseclass.
  192. The heavy lifting is done by the subclasses.
  193. """
  194. super().__init__(*system)
  195. def impulse(self, X0=None, T=None, N=None):
  196. """
  197. Return the impulse response of a continuous-time system.
  198. See `impulse` for details.
  199. """
  200. return impulse(self, X0=X0, T=T, N=N)
  201. def step(self, X0=None, T=None, N=None):
  202. """
  203. Return the step response of a continuous-time system.
  204. See `step` for details.
  205. """
  206. return step(self, X0=X0, T=T, N=N)
  207. def output(self, U, T, X0=None):
  208. """
  209. Return the response of a continuous-time system to input `U`.
  210. See `lsim` for details.
  211. """
  212. return lsim(self, U, T, X0=X0)
  213. def bode(self, w=None, n=100):
  214. """
  215. Calculate Bode magnitude and phase data of a continuous-time system.
  216. Returns a 3-tuple containing arrays of frequencies [rad/s], magnitude
  217. [dB] and phase [deg]. See `bode` for details.
  218. Examples
  219. --------
  220. >>> from scipy import signal
  221. >>> import matplotlib.pyplot as plt
  222. >>> sys = signal.TransferFunction([1], [1, 1])
  223. >>> w, mag, phase = sys.bode()
  224. >>> plt.figure()
  225. >>> plt.semilogx(w, mag) # Bode magnitude plot
  226. >>> plt.figure()
  227. >>> plt.semilogx(w, phase) # Bode phase plot
  228. >>> plt.show()
  229. """
  230. return bode(self, w=w, n=n)
  231. def freqresp(self, w=None, n=10000):
  232. """
  233. Calculate the frequency response of a continuous-time system.
  234. Returns a 2-tuple containing arrays of frequencies [rad/s] and
  235. complex magnitude.
  236. See `freqresp` for details.
  237. """
  238. return freqresp(self, w=w, n=n)
  239. def to_discrete(self, dt, method='zoh', alpha=None):
  240. """Return a discretized version of the current system.
  241. Parameters: See `cont2discrete` for details.
  242. Returns
  243. -------
  244. sys: instance of `dlti`
  245. """
  246. raise NotImplementedError('to_discrete is not implemented for this '
  247. 'system class.')
  248. class dlti(LinearTimeInvariant):
  249. r"""
  250. Discrete-time linear time invariant system base class.
  251. Parameters
  252. ----------
  253. *system: arguments
  254. The `dlti` class can be instantiated with either 2, 3 or 4 arguments.
  255. The following gives the number of arguments and the corresponding
  256. discrete-time subclass that is created:
  257. * 2: `TransferFunction`: (numerator, denominator)
  258. * 3: `ZerosPolesGain`: (zeros, poles, gain)
  259. * 4: `StateSpace`: (A, B, C, D)
  260. Each argument can be an array or a sequence.
  261. dt: float, optional
  262. Sampling time [s] of the discrete-time systems. Defaults to ``True``
  263. (unspecified sampling time). Must be specified as a keyword argument,
  264. for example, ``dt=0.1``.
  265. See Also
  266. --------
  267. ZerosPolesGain, StateSpace, TransferFunction, lti
  268. Notes
  269. -----
  270. `dlti` instances do not exist directly. Instead, `dlti` creates an instance
  271. of one of its subclasses: `StateSpace`, `TransferFunction` or
  272. `ZerosPolesGain`.
  273. Changing the value of properties that are not directly part of the current
  274. system representation (such as the `zeros` of a `StateSpace` system) is
  275. very inefficient and may lead to numerical inaccuracies. It is better to
  276. convert to the specific system representation first. For example, call
  277. ``sys = sys.to_zpk()`` before accessing/changing the zeros, poles or gain.
  278. If (numerator, denominator) is passed in for ``*system``, coefficients for
  279. both the numerator and denominator should be specified in descending
  280. exponent order (e.g., ``z^2 + 3z + 5`` would be represented as ``[1, 3,
  281. 5]``).
  282. .. versionadded:: 0.18.0
  283. Examples
  284. --------
  285. >>> from scipy import signal
  286. >>> signal.dlti(1, 2, 3, 4)
  287. StateSpaceDiscrete(
  288. array([[1]]),
  289. array([[2]]),
  290. array([[3]]),
  291. array([[4]]),
  292. dt: True
  293. )
  294. >>> signal.dlti(1, 2, 3, 4, dt=0.1)
  295. StateSpaceDiscrete(
  296. array([[1]]),
  297. array([[2]]),
  298. array([[3]]),
  299. array([[4]]),
  300. dt: 0.1
  301. )
  302. Construct the transfer function
  303. :math:`H(z) = \frac{5(z - 1)(z - 2)}{(z - 3)(z - 4)}` with a sampling time
  304. of 0.1 seconds:
  305. >>> signal.dlti([1, 2], [3, 4], 5, dt=0.1)
  306. ZerosPolesGainDiscrete(
  307. array([1, 2]),
  308. array([3, 4]),
  309. 5,
  310. dt: 0.1
  311. )
  312. Construct the transfer function :math:`H(z) = \frac{3z + 4}{1z + 2}` with
  313. a sampling time of 0.1 seconds:
  314. >>> signal.dlti([3, 4], [1, 2], dt=0.1)
  315. TransferFunctionDiscrete(
  316. array([3., 4.]),
  317. array([1., 2.]),
  318. dt: 0.1
  319. )
  320. """
  321. def __new__(cls, *system, **kwargs):
  322. """Create an instance of the appropriate subclass."""
  323. if cls is dlti:
  324. N = len(system)
  325. if N == 2:
  326. return TransferFunctionDiscrete.__new__(
  327. TransferFunctionDiscrete, *system, **kwargs)
  328. elif N == 3:
  329. return ZerosPolesGainDiscrete.__new__(ZerosPolesGainDiscrete,
  330. *system, **kwargs)
  331. elif N == 4:
  332. return StateSpaceDiscrete.__new__(StateSpaceDiscrete, *system,
  333. **kwargs)
  334. else:
  335. raise ValueError("`system` needs to be an instance of `dlti` "
  336. "or have 2, 3 or 4 arguments.")
  337. # __new__ was called from a subclass, let it call its own functions
  338. return super().__new__(cls)
  339. def __init__(self, *system, **kwargs):
  340. """
  341. Initialize the `lti` baseclass.
  342. The heavy lifting is done by the subclasses.
  343. """
  344. dt = kwargs.pop('dt', True)
  345. super().__init__(*system, **kwargs)
  346. self.dt = dt
  347. @property
  348. def dt(self):
  349. """Return the sampling time of the system."""
  350. return self._dt
  351. @dt.setter
  352. def dt(self, dt):
  353. self._dt = dt
  354. def impulse(self, x0=None, t=None, n=None):
  355. """
  356. Return the impulse response of the discrete-time `dlti` system.
  357. See `dimpulse` for details.
  358. """
  359. return dimpulse(self, x0=x0, t=t, n=n)
  360. def step(self, x0=None, t=None, n=None):
  361. """
  362. Return the step response of the discrete-time `dlti` system.
  363. See `dstep` for details.
  364. """
  365. return dstep(self, x0=x0, t=t, n=n)
  366. def output(self, u, t, x0=None):
  367. """
  368. Return the response of the discrete-time system to input `u`.
  369. See `dlsim` for details.
  370. """
  371. return dlsim(self, u, t, x0=x0)
  372. def bode(self, w=None, n=100):
  373. r"""
  374. Calculate Bode magnitude and phase data of a discrete-time system.
  375. Returns a 3-tuple containing arrays of frequencies [rad/s], magnitude
  376. [dB] and phase [deg]. See `dbode` for details.
  377. Examples
  378. --------
  379. >>> from scipy import signal
  380. >>> import matplotlib.pyplot as plt
  381. Construct the transfer function :math:`H(z) = \frac{1}{z^2 + 2z + 3}`
  382. with sampling time 0.5s:
  383. >>> sys = signal.TransferFunction([1], [1, 2, 3], dt=0.5)
  384. Equivalent: signal.dbode(sys)
  385. >>> w, mag, phase = sys.bode()
  386. >>> plt.figure()
  387. >>> plt.semilogx(w, mag) # Bode magnitude plot
  388. >>> plt.figure()
  389. >>> plt.semilogx(w, phase) # Bode phase plot
  390. >>> plt.show()
  391. """
  392. return dbode(self, w=w, n=n)
  393. def freqresp(self, w=None, n=10000, whole=False):
  394. """
  395. Calculate the frequency response of a discrete-time system.
  396. Returns a 2-tuple containing arrays of frequencies [rad/s] and
  397. complex magnitude.
  398. See `dfreqresp` for details.
  399. """
  400. return dfreqresp(self, w=w, n=n, whole=whole)
  401. class TransferFunction(LinearTimeInvariant):
  402. r"""Linear Time Invariant system class in transfer function form.
  403. Represents the system as the continuous-time transfer function
  404. :math:`H(s)=\sum_{i=0}^N b[N-i] s^i / \sum_{j=0}^M a[M-j] s^j` or the
  405. discrete-time transfer function
  406. :math:`H(z)=\sum_{i=0}^N b[N-i] z^i / \sum_{j=0}^M a[M-j] z^j`, where
  407. :math:`b` are elements of the numerator `num`, :math:`a` are elements of
  408. the denominator `den`, and ``N == len(b) - 1``, ``M == len(a) - 1``.
  409. `TransferFunction` systems inherit additional
  410. functionality from the `lti`, respectively the `dlti` classes, depending on
  411. which system representation is used.
  412. Parameters
  413. ----------
  414. *system: arguments
  415. The `TransferFunction` class can be instantiated with 1 or 2
  416. arguments. The following gives the number of input arguments and their
  417. interpretation:
  418. * 1: `lti` or `dlti` system: (`StateSpace`, `TransferFunction` or
  419. `ZerosPolesGain`)
  420. * 2: array_like: (numerator, denominator)
  421. dt: float, optional
  422. Sampling time [s] of the discrete-time systems. Defaults to `None`
  423. (continuous-time). Must be specified as a keyword argument, for
  424. example, ``dt=0.1``.
  425. See Also
  426. --------
  427. ZerosPolesGain, StateSpace, lti, dlti
  428. tf2ss, tf2zpk, tf2sos
  429. Notes
  430. -----
  431. Changing the value of properties that are not part of the
  432. `TransferFunction` system representation (such as the `A`, `B`, `C`, `D`
  433. state-space matrices) is very inefficient and may lead to numerical
  434. inaccuracies. It is better to convert to the specific system
  435. representation first. For example, call ``sys = sys.to_ss()`` before
  436. accessing/changing the A, B, C, D system matrices.
  437. If (numerator, denominator) is passed in for ``*system``, coefficients
  438. for both the numerator and denominator should be specified in descending
  439. exponent order (e.g. ``s^2 + 3s + 5`` or ``z^2 + 3z + 5`` would be
  440. represented as ``[1, 3, 5]``)
  441. Examples
  442. --------
  443. Construct the transfer function
  444. :math:`H(s) = \frac{s^2 + 3s + 3}{s^2 + 2s + 1}`:
  445. >>> from scipy import signal
  446. >>> num = [1, 3, 3]
  447. >>> den = [1, 2, 1]
  448. >>> signal.TransferFunction(num, den)
  449. TransferFunctionContinuous(
  450. array([1., 3., 3.]),
  451. array([1., 2., 1.]),
  452. dt: None
  453. )
  454. Construct the transfer function
  455. :math:`H(z) = \frac{z^2 + 3z + 3}{z^2 + 2z + 1}` with a sampling time of
  456. 0.1 seconds:
  457. >>> signal.TransferFunction(num, den, dt=0.1)
  458. TransferFunctionDiscrete(
  459. array([1., 3., 3.]),
  460. array([1., 2., 1.]),
  461. dt: 0.1
  462. )
  463. """
  464. def __new__(cls, *system, **kwargs):
  465. """Handle object conversion if input is an instance of lti."""
  466. if len(system) == 1 and isinstance(system[0], LinearTimeInvariant):
  467. return system[0].to_tf()
  468. # Choose whether to inherit from `lti` or from `dlti`
  469. if cls is TransferFunction:
  470. if kwargs.get('dt') is None:
  471. return TransferFunctionContinuous.__new__(
  472. TransferFunctionContinuous,
  473. *system,
  474. **kwargs)
  475. else:
  476. return TransferFunctionDiscrete.__new__(
  477. TransferFunctionDiscrete,
  478. *system,
  479. **kwargs)
  480. # No special conversion needed
  481. return super().__new__(cls)
  482. def __init__(self, *system, **kwargs):
  483. """Initialize the state space LTI system."""
  484. # Conversion of lti instances is handled in __new__
  485. if isinstance(system[0], LinearTimeInvariant):
  486. return
  487. # Remove system arguments, not needed by parents anymore
  488. super().__init__(**kwargs)
  489. self._num = None
  490. self._den = None
  491. self.num, self.den = normalize(*system)
  492. def __repr__(self):
  493. """Return representation of the system's transfer function"""
  494. return (
  495. f'{self.__class__.__name__}(\n'
  496. f'{repr(self.num)},\n'
  497. f'{repr(self.den)},\n'
  498. f'dt: {repr(self.dt)}\n)'
  499. )
  500. @property
  501. def num(self):
  502. """Numerator of the `TransferFunction` system."""
  503. return self._num
  504. @num.setter
  505. def num(self, num):
  506. self._num = atleast_1d(num)
  507. # Update dimensions
  508. if len(self.num.shape) > 1:
  509. self.outputs, self.inputs = self.num.shape
  510. else:
  511. self.outputs = 1
  512. self.inputs = 1
  513. @property
  514. def den(self):
  515. """Denominator of the `TransferFunction` system."""
  516. return self._den
  517. @den.setter
  518. def den(self, den):
  519. self._den = atleast_1d(den)
  520. def _copy(self, system):
  521. """
  522. Copy the parameters of another `TransferFunction` object
  523. Parameters
  524. ----------
  525. system : `TransferFunction`
  526. The `StateSpace` system that is to be copied
  527. """
  528. self.num = system.num
  529. self.den = system.den
  530. def to_tf(self):
  531. """
  532. Return a copy of the current `TransferFunction` system.
  533. Returns
  534. -------
  535. sys : instance of `TransferFunction`
  536. The current system (copy)
  537. """
  538. return copy.deepcopy(self)
  539. def to_zpk(self):
  540. """
  541. Convert system representation to `ZerosPolesGain`.
  542. Returns
  543. -------
  544. sys : instance of `ZerosPolesGain`
  545. Zeros, poles, gain representation of the current system
  546. """
  547. return ZerosPolesGain(*tf2zpk(self.num, self.den),
  548. **self._dt_dict)
  549. def to_ss(self):
  550. """
  551. Convert system representation to `StateSpace`.
  552. Returns
  553. -------
  554. sys : instance of `StateSpace`
  555. State space model of the current system
  556. """
  557. return StateSpace(*tf2ss(self.num, self.den),
  558. **self._dt_dict)
  559. @staticmethod
  560. def _z_to_zinv(num, den):
  561. """Change a transfer function from the variable `z` to `z**-1`.
  562. Parameters
  563. ----------
  564. num, den: 1d array_like
  565. Sequences representing the coefficients of the numerator and
  566. denominator polynomials, in order of descending degree of 'z'.
  567. That is, ``5z**2 + 3z + 2`` is presented as ``[5, 3, 2]``.
  568. Returns
  569. -------
  570. num, den: 1d array_like
  571. Sequences representing the coefficients of the numerator and
  572. denominator polynomials, in order of ascending degree of 'z**-1'.
  573. That is, ``5 + 3 z**-1 + 2 z**-2`` is presented as ``[5, 3, 2]``.
  574. """
  575. diff = len(num) - len(den)
  576. if diff > 0:
  577. den = np.hstack((np.zeros(diff), den))
  578. elif diff < 0:
  579. num = np.hstack((np.zeros(-diff), num))
  580. return num, den
  581. @staticmethod
  582. def _zinv_to_z(num, den):
  583. """Change a transfer function from the variable `z` to `z**-1`.
  584. Parameters
  585. ----------
  586. num, den: 1d array_like
  587. Sequences representing the coefficients of the numerator and
  588. denominator polynomials, in order of ascending degree of 'z**-1'.
  589. That is, ``5 + 3 z**-1 + 2 z**-2`` is presented as ``[5, 3, 2]``.
  590. Returns
  591. -------
  592. num, den: 1d array_like
  593. Sequences representing the coefficients of the numerator and
  594. denominator polynomials, in order of descending degree of 'z'.
  595. That is, ``5z**2 + 3z + 2`` is presented as ``[5, 3, 2]``.
  596. """
  597. diff = len(num) - len(den)
  598. if diff > 0:
  599. den = np.hstack((den, np.zeros(diff)))
  600. elif diff < 0:
  601. num = np.hstack((num, np.zeros(-diff)))
  602. return num, den
  603. class TransferFunctionContinuous(TransferFunction, lti):
  604. r"""
  605. Continuous-time Linear Time Invariant system in transfer function form.
  606. Represents the system as the transfer function
  607. :math:`H(s)=\sum_{i=0}^N b[N-i] s^i / \sum_{j=0}^M a[M-j] s^j`, where
  608. :math:`b` are elements of the numerator `num`, :math:`a` are elements of
  609. the denominator `den`, and ``N == len(b) - 1``, ``M == len(a) - 1``.
  610. Continuous-time `TransferFunction` systems inherit additional
  611. functionality from the `lti` class.
  612. Parameters
  613. ----------
  614. *system: arguments
  615. The `TransferFunction` class can be instantiated with 1 or 2
  616. arguments. The following gives the number of input arguments and their
  617. interpretation:
  618. * 1: `lti` system: (`StateSpace`, `TransferFunction` or
  619. `ZerosPolesGain`)
  620. * 2: array_like: (numerator, denominator)
  621. See Also
  622. --------
  623. ZerosPolesGain, StateSpace, lti
  624. tf2ss, tf2zpk, tf2sos
  625. Notes
  626. -----
  627. Changing the value of properties that are not part of the
  628. `TransferFunction` system representation (such as the `A`, `B`, `C`, `D`
  629. state-space matrices) is very inefficient and may lead to numerical
  630. inaccuracies. It is better to convert to the specific system
  631. representation first. For example, call ``sys = sys.to_ss()`` before
  632. accessing/changing the A, B, C, D system matrices.
  633. If (numerator, denominator) is passed in for ``*system``, coefficients
  634. for both the numerator and denominator should be specified in descending
  635. exponent order (e.g. ``s^2 + 3s + 5`` would be represented as
  636. ``[1, 3, 5]``)
  637. Examples
  638. --------
  639. Construct the transfer function
  640. :math:`H(s) = \frac{s^2 + 3s + 3}{s^2 + 2s + 1}`:
  641. >>> from scipy import signal
  642. >>> num = [1, 3, 3]
  643. >>> den = [1, 2, 1]
  644. >>> signal.TransferFunction(num, den)
  645. TransferFunctionContinuous(
  646. array([ 1., 3., 3.]),
  647. array([ 1., 2., 1.]),
  648. dt: None
  649. )
  650. """
  651. def to_discrete(self, dt, method='zoh', alpha=None):
  652. """
  653. Returns the discretized `TransferFunction` system.
  654. Parameters: See `cont2discrete` for details.
  655. Returns
  656. -------
  657. sys: instance of `dlti` and `StateSpace`
  658. """
  659. return TransferFunction(*cont2discrete((self.num, self.den),
  660. dt,
  661. method=method,
  662. alpha=alpha)[:-1],
  663. dt=dt)
  664. class TransferFunctionDiscrete(TransferFunction, dlti):
  665. r"""
  666. Discrete-time Linear Time Invariant system in transfer function form.
  667. Represents the system as the transfer function
  668. :math:`H(z)=\sum_{i=0}^N b[N-i] z^i / \sum_{j=0}^M a[M-j] z^j`, where
  669. :math:`b` are elements of the numerator `num`, :math:`a` are elements of
  670. the denominator `den`, and ``N == len(b) - 1``, ``M == len(a) - 1``.
  671. Discrete-time `TransferFunction` systems inherit additional functionality
  672. from the `dlti` class.
  673. Parameters
  674. ----------
  675. *system: arguments
  676. The `TransferFunction` class can be instantiated with 1 or 2
  677. arguments. The following gives the number of input arguments and their
  678. interpretation:
  679. * 1: `dlti` system: (`StateSpace`, `TransferFunction` or
  680. `ZerosPolesGain`)
  681. * 2: array_like: (numerator, denominator)
  682. dt: float, optional
  683. Sampling time [s] of the discrete-time systems. Defaults to `True`
  684. (unspecified sampling time). Must be specified as a keyword argument,
  685. for example, ``dt=0.1``.
  686. See Also
  687. --------
  688. ZerosPolesGain, StateSpace, dlti
  689. tf2ss, tf2zpk, tf2sos
  690. Notes
  691. -----
  692. Changing the value of properties that are not part of the
  693. `TransferFunction` system representation (such as the `A`, `B`, `C`, `D`
  694. state-space matrices) is very inefficient and may lead to numerical
  695. inaccuracies.
  696. If (numerator, denominator) is passed in for ``*system``, coefficients
  697. for both the numerator and denominator should be specified in descending
  698. exponent order (e.g., ``z^2 + 3z + 5`` would be represented as
  699. ``[1, 3, 5]``).
  700. Examples
  701. --------
  702. Construct the transfer function
  703. :math:`H(z) = \frac{z^2 + 3z + 3}{z^2 + 2z + 1}` with a sampling time of
  704. 0.5 seconds:
  705. >>> from scipy import signal
  706. >>> num = [1, 3, 3]
  707. >>> den = [1, 2, 1]
  708. >>> signal.TransferFunction(num, den, dt=0.5)
  709. TransferFunctionDiscrete(
  710. array([ 1., 3., 3.]),
  711. array([ 1., 2., 1.]),
  712. dt: 0.5
  713. )
  714. """
  715. pass
  716. class ZerosPolesGain(LinearTimeInvariant):
  717. r"""
  718. Linear Time Invariant system class in zeros, poles, gain form.
  719. Represents the system as the continuous- or discrete-time transfer function
  720. :math:`H(s)=k \prod_i (s - z[i]) / \prod_j (s - p[j])`, where :math:`k` is
  721. the `gain`, :math:`z` are the `zeros` and :math:`p` are the `poles`.
  722. `ZerosPolesGain` systems inherit additional functionality from the `lti`,
  723. respectively the `dlti` classes, depending on which system representation
  724. is used.
  725. Parameters
  726. ----------
  727. *system : arguments
  728. The `ZerosPolesGain` class can be instantiated with 1 or 3
  729. arguments. The following gives the number of input arguments and their
  730. interpretation:
  731. * 1: `lti` or `dlti` system: (`StateSpace`, `TransferFunction` or
  732. `ZerosPolesGain`)
  733. * 3: array_like: (zeros, poles, gain)
  734. dt: float, optional
  735. Sampling time [s] of the discrete-time systems. Defaults to `None`
  736. (continuous-time). Must be specified as a keyword argument, for
  737. example, ``dt=0.1``.
  738. See Also
  739. --------
  740. TransferFunction, StateSpace, lti, dlti
  741. zpk2ss, zpk2tf, zpk2sos
  742. Notes
  743. -----
  744. Changing the value of properties that are not part of the
  745. `ZerosPolesGain` system representation (such as the `A`, `B`, `C`, `D`
  746. state-space matrices) is very inefficient and may lead to numerical
  747. inaccuracies. It is better to convert to the specific system
  748. representation first. For example, call ``sys = sys.to_ss()`` before
  749. accessing/changing the A, B, C, D system matrices.
  750. Examples
  751. --------
  752. Construct the transfer function
  753. :math:`H(s) = \frac{5(s - 1)(s - 2)}{(s - 3)(s - 4)}`:
  754. >>> from scipy import signal
  755. >>> signal.ZerosPolesGain([1, 2], [3, 4], 5)
  756. ZerosPolesGainContinuous(
  757. array([1, 2]),
  758. array([3, 4]),
  759. 5,
  760. dt: None
  761. )
  762. Construct the transfer function
  763. :math:`H(z) = \frac{5(z - 1)(z - 2)}{(z - 3)(z - 4)}` with a sampling time
  764. of 0.1 seconds:
  765. >>> signal.ZerosPolesGain([1, 2], [3, 4], 5, dt=0.1)
  766. ZerosPolesGainDiscrete(
  767. array([1, 2]),
  768. array([3, 4]),
  769. 5,
  770. dt: 0.1
  771. )
  772. """
  773. def __new__(cls, *system, **kwargs):
  774. """Handle object conversion if input is an instance of `lti`"""
  775. if len(system) == 1 and isinstance(system[0], LinearTimeInvariant):
  776. return system[0].to_zpk()
  777. # Choose whether to inherit from `lti` or from `dlti`
  778. if cls is ZerosPolesGain:
  779. if kwargs.get('dt') is None:
  780. return ZerosPolesGainContinuous.__new__(
  781. ZerosPolesGainContinuous,
  782. *system,
  783. **kwargs)
  784. else:
  785. return ZerosPolesGainDiscrete.__new__(
  786. ZerosPolesGainDiscrete,
  787. *system,
  788. **kwargs
  789. )
  790. # No special conversion needed
  791. return super().__new__(cls)
  792. def __init__(self, *system, **kwargs):
  793. """Initialize the zeros, poles, gain system."""
  794. # Conversion of lti instances is handled in __new__
  795. if isinstance(system[0], LinearTimeInvariant):
  796. return
  797. super().__init__(**kwargs)
  798. self._zeros = None
  799. self._poles = None
  800. self._gain = None
  801. self.zeros, self.poles, self.gain = system
  802. def __repr__(self):
  803. """Return representation of the `ZerosPolesGain` system."""
  804. return (
  805. f'{self.__class__.__name__}(\n'
  806. f'{repr(self.zeros)},\n'
  807. f'{repr(self.poles)},\n'
  808. f'{repr(self.gain)},\n'
  809. f'dt: {repr(self.dt)}\n)'
  810. )
  811. @property
  812. def zeros(self):
  813. """Zeros of the `ZerosPolesGain` system."""
  814. return self._zeros
  815. @zeros.setter
  816. def zeros(self, zeros):
  817. self._zeros = atleast_1d(zeros)
  818. # Update dimensions
  819. if len(self.zeros.shape) > 1:
  820. self.outputs, self.inputs = self.zeros.shape
  821. else:
  822. self.outputs = 1
  823. self.inputs = 1
  824. @property
  825. def poles(self):
  826. """Poles of the `ZerosPolesGain` system."""
  827. return self._poles
  828. @poles.setter
  829. def poles(self, poles):
  830. self._poles = atleast_1d(poles)
  831. @property
  832. def gain(self):
  833. """Gain of the `ZerosPolesGain` system."""
  834. return self._gain
  835. @gain.setter
  836. def gain(self, gain):
  837. self._gain = gain
  838. def _copy(self, system):
  839. """
  840. Copy the parameters of another `ZerosPolesGain` system.
  841. Parameters
  842. ----------
  843. system : instance of `ZerosPolesGain`
  844. The zeros, poles gain system that is to be copied
  845. """
  846. self.poles = system.poles
  847. self.zeros = system.zeros
  848. self.gain = system.gain
  849. def to_tf(self):
  850. """
  851. Convert system representation to `TransferFunction`.
  852. Returns
  853. -------
  854. sys : instance of `TransferFunction`
  855. Transfer function of the current system
  856. """
  857. return TransferFunction(*zpk2tf(self.zeros, self.poles, self.gain),
  858. **self._dt_dict)
  859. def to_zpk(self):
  860. """
  861. Return a copy of the current 'ZerosPolesGain' system.
  862. Returns
  863. -------
  864. sys : instance of `ZerosPolesGain`
  865. The current system (copy)
  866. """
  867. return copy.deepcopy(self)
  868. def to_ss(self):
  869. """
  870. Convert system representation to `StateSpace`.
  871. Returns
  872. -------
  873. sys : instance of `StateSpace`
  874. State space model of the current system
  875. """
  876. return StateSpace(*zpk2ss(self.zeros, self.poles, self.gain),
  877. **self._dt_dict)
  878. class ZerosPolesGainContinuous(ZerosPolesGain, lti):
  879. r"""
  880. Continuous-time Linear Time Invariant system in zeros, poles, gain form.
  881. Represents the system as the continuous time transfer function
  882. :math:`H(s)=k \prod_i (s - z[i]) / \prod_j (s - p[j])`, where :math:`k` is
  883. the `gain`, :math:`z` are the `zeros` and :math:`p` are the `poles`.
  884. Continuous-time `ZerosPolesGain` systems inherit additional functionality
  885. from the `lti` class.
  886. Parameters
  887. ----------
  888. *system : arguments
  889. The `ZerosPolesGain` class can be instantiated with 1 or 3
  890. arguments. The following gives the number of input arguments and their
  891. interpretation:
  892. * 1: `lti` system: (`StateSpace`, `TransferFunction` or
  893. `ZerosPolesGain`)
  894. * 3: array_like: (zeros, poles, gain)
  895. See Also
  896. --------
  897. TransferFunction, StateSpace, lti
  898. zpk2ss, zpk2tf, zpk2sos
  899. Notes
  900. -----
  901. Changing the value of properties that are not part of the
  902. `ZerosPolesGain` system representation (such as the `A`, `B`, `C`, `D`
  903. state-space matrices) is very inefficient and may lead to numerical
  904. inaccuracies. It is better to convert to the specific system
  905. representation first. For example, call ``sys = sys.to_ss()`` before
  906. accessing/changing the A, B, C, D system matrices.
  907. Examples
  908. --------
  909. Construct the transfer function
  910. :math:`H(s)=\frac{5(s - 1)(s - 2)}{(s - 3)(s - 4)}`:
  911. >>> from scipy import signal
  912. >>> signal.ZerosPolesGain([1, 2], [3, 4], 5)
  913. ZerosPolesGainContinuous(
  914. array([1, 2]),
  915. array([3, 4]),
  916. 5,
  917. dt: None
  918. )
  919. """
  920. def to_discrete(self, dt, method='zoh', alpha=None):
  921. """
  922. Returns the discretized `ZerosPolesGain` system.
  923. Parameters: See `cont2discrete` for details.
  924. Returns
  925. -------
  926. sys: instance of `dlti` and `ZerosPolesGain`
  927. """
  928. return ZerosPolesGain(
  929. *cont2discrete((self.zeros, self.poles, self.gain),
  930. dt,
  931. method=method,
  932. alpha=alpha)[:-1],
  933. dt=dt)
  934. class ZerosPolesGainDiscrete(ZerosPolesGain, dlti):
  935. r"""
  936. Discrete-time Linear Time Invariant system in zeros, poles, gain form.
  937. Represents the system as the discrete-time transfer function
  938. :math:`H(z)=k \prod_i (z - q[i]) / \prod_j (z - p[j])`, where :math:`k` is
  939. the `gain`, :math:`q` are the `zeros` and :math:`p` are the `poles`.
  940. Discrete-time `ZerosPolesGain` systems inherit additional functionality
  941. from the `dlti` class.
  942. Parameters
  943. ----------
  944. *system : arguments
  945. The `ZerosPolesGain` class can be instantiated with 1 or 3
  946. arguments. The following gives the number of input arguments and their
  947. interpretation:
  948. * 1: `dlti` system: (`StateSpace`, `TransferFunction` or
  949. `ZerosPolesGain`)
  950. * 3: array_like: (zeros, poles, gain)
  951. dt: float, optional
  952. Sampling time [s] of the discrete-time systems. Defaults to `True`
  953. (unspecified sampling time). Must be specified as a keyword argument,
  954. for example, ``dt=0.1``.
  955. See Also
  956. --------
  957. TransferFunction, StateSpace, dlti
  958. zpk2ss, zpk2tf, zpk2sos
  959. Notes
  960. -----
  961. Changing the value of properties that are not part of the
  962. `ZerosPolesGain` system representation (such as the `A`, `B`, `C`, `D`
  963. state-space matrices) is very inefficient and may lead to numerical
  964. inaccuracies. It is better to convert to the specific system
  965. representation first. For example, call ``sys = sys.to_ss()`` before
  966. accessing/changing the A, B, C, D system matrices.
  967. Examples
  968. --------
  969. Construct the transfer function
  970. :math:`H(s) = \frac{5(s - 1)(s - 2)}{(s - 3)(s - 4)}`:
  971. >>> from scipy import signal
  972. >>> signal.ZerosPolesGain([1, 2], [3, 4], 5)
  973. ZerosPolesGainContinuous(
  974. array([1, 2]),
  975. array([3, 4]),
  976. 5,
  977. dt: None
  978. )
  979. Construct the transfer function
  980. :math:`H(z) = \frac{5(z - 1)(z - 2)}{(z - 3)(z - 4)}` with a sampling time
  981. of 0.1 seconds:
  982. >>> signal.ZerosPolesGain([1, 2], [3, 4], 5, dt=0.1)
  983. ZerosPolesGainDiscrete(
  984. array([1, 2]),
  985. array([3, 4]),
  986. 5,
  987. dt: 0.1
  988. )
  989. """
  990. pass
  991. class StateSpace(LinearTimeInvariant):
  992. r"""
  993. Linear Time Invariant system in state-space form.
  994. Represents the system as the continuous-time, first order differential
  995. equation :math:`\dot{x} = A x + B u` or the discrete-time difference
  996. equation :math:`x[k+1] = A x[k] + B u[k]`. `StateSpace` systems
  997. inherit additional functionality from the `lti`, respectively the `dlti`
  998. classes, depending on which system representation is used.
  999. Parameters
  1000. ----------
  1001. *system: arguments
  1002. The `StateSpace` class can be instantiated with 1 or 4 arguments.
  1003. The following gives the number of input arguments and their
  1004. interpretation:
  1005. * 1: `lti` or `dlti` system: (`StateSpace`, `TransferFunction` or
  1006. `ZerosPolesGain`)
  1007. * 4: array_like: (A, B, C, D)
  1008. dt: float, optional
  1009. Sampling time [s] of the discrete-time systems. Defaults to `None`
  1010. (continuous-time). Must be specified as a keyword argument, for
  1011. example, ``dt=0.1``.
  1012. See Also
  1013. --------
  1014. TransferFunction, ZerosPolesGain, lti, dlti
  1015. ss2zpk, ss2tf, zpk2sos
  1016. Notes
  1017. -----
  1018. Changing the value of properties that are not part of the
  1019. `StateSpace` system representation (such as `zeros` or `poles`) is very
  1020. inefficient and may lead to numerical inaccuracies. It is better to
  1021. convert to the specific system representation first. For example, call
  1022. ``sys = sys.to_zpk()`` before accessing/changing the zeros, poles or gain.
  1023. Examples
  1024. --------
  1025. >>> from scipy import signal
  1026. >>> import numpy as np
  1027. >>> a = np.array([[0, 1], [0, 0]])
  1028. >>> b = np.array([[0], [1]])
  1029. >>> c = np.array([[1, 0]])
  1030. >>> d = np.array([[0]])
  1031. >>> sys = signal.StateSpace(a, b, c, d)
  1032. >>> print(sys)
  1033. StateSpaceContinuous(
  1034. array([[0, 1],
  1035. [0, 0]]),
  1036. array([[0],
  1037. [1]]),
  1038. array([[1, 0]]),
  1039. array([[0]]),
  1040. dt: None
  1041. )
  1042. >>> sys.to_discrete(0.1)
  1043. StateSpaceDiscrete(
  1044. array([[1. , 0.1],
  1045. [0. , 1. ]]),
  1046. array([[0.005],
  1047. [0.1 ]]),
  1048. array([[1, 0]]),
  1049. array([[0]]),
  1050. dt: 0.1
  1051. )
  1052. >>> a = np.array([[1, 0.1], [0, 1]])
  1053. >>> b = np.array([[0.005], [0.1]])
  1054. >>> signal.StateSpace(a, b, c, d, dt=0.1)
  1055. StateSpaceDiscrete(
  1056. array([[1. , 0.1],
  1057. [0. , 1. ]]),
  1058. array([[0.005],
  1059. [0.1 ]]),
  1060. array([[1, 0]]),
  1061. array([[0]]),
  1062. dt: 0.1
  1063. )
  1064. """
  1065. # Override NumPy binary operations and ufuncs
  1066. __array_priority__ = 100.0
  1067. __array_ufunc__ = None
  1068. def __new__(cls, *system, **kwargs):
  1069. """Create new StateSpace object and settle inheritance."""
  1070. # Handle object conversion if input is an instance of `lti`
  1071. if len(system) == 1 and isinstance(system[0], LinearTimeInvariant):
  1072. return system[0].to_ss()
  1073. # Choose whether to inherit from `lti` or from `dlti`
  1074. if cls is StateSpace:
  1075. if kwargs.get('dt') is None:
  1076. return StateSpaceContinuous.__new__(StateSpaceContinuous,
  1077. *system, **kwargs)
  1078. else:
  1079. return StateSpaceDiscrete.__new__(StateSpaceDiscrete,
  1080. *system, **kwargs)
  1081. # No special conversion needed
  1082. return super().__new__(cls)
  1083. def __init__(self, *system, **kwargs):
  1084. """Initialize the state space lti/dlti system."""
  1085. # Conversion of lti instances is handled in __new__
  1086. if isinstance(system[0], LinearTimeInvariant):
  1087. return
  1088. # Remove system arguments, not needed by parents anymore
  1089. super().__init__(**kwargs)
  1090. self._A = None
  1091. self._B = None
  1092. self._C = None
  1093. self._D = None
  1094. self.A, self.B, self.C, self.D = abcd_normalize(*system)
  1095. def __repr__(self):
  1096. """Return representation of the `StateSpace` system."""
  1097. return (
  1098. f'{self.__class__.__name__}(\n'
  1099. f'{repr(self.A)},\n'
  1100. f'{repr(self.B)},\n'
  1101. f'{repr(self.C)},\n'
  1102. f'{repr(self.D)},\n'
  1103. f'dt: {repr(self.dt)}\n)'
  1104. )
  1105. def _check_binop_other(self, other):
  1106. return isinstance(other, StateSpace | np.ndarray | float | complex |
  1107. np.number | int)
  1108. def __mul__(self, other):
  1109. """
  1110. Post-multiply another system or a scalar
  1111. Handles multiplication of systems in the sense of a frequency domain
  1112. multiplication. That means, given two systems E1(s) and E2(s), their
  1113. multiplication, H(s) = E1(s) * E2(s), means that applying H(s) to U(s)
  1114. is equivalent to first applying E2(s), and then E1(s).
  1115. Notes
  1116. -----
  1117. For SISO systems the order of system application does not matter.
  1118. However, for MIMO systems, where the two systems are matrices, the
  1119. order above ensures standard Matrix multiplication rules apply.
  1120. """
  1121. if not self._check_binop_other(other):
  1122. return NotImplemented
  1123. if isinstance(other, StateSpace):
  1124. # Disallow mix of discrete and continuous systems.
  1125. if type(other) is not type(self):
  1126. return NotImplemented
  1127. if self.dt != other.dt:
  1128. raise TypeError('Cannot multiply systems with different `dt`.')
  1129. n1 = self.A.shape[0]
  1130. n2 = other.A.shape[0]
  1131. # Interconnection of systems
  1132. # x1' = A1 x1 + B1 u1
  1133. # y1 = C1 x1 + D1 u1
  1134. # x2' = A2 x2 + B2 y1
  1135. # y2 = C2 x2 + D2 y1
  1136. #
  1137. # Plugging in with u1 = y2 yields
  1138. # [x1'] [A1 B1*C2 ] [x1] [B1*D2]
  1139. # [x2'] = [0 A2 ] [x2] + [B2 ] u2
  1140. # [x1]
  1141. # y2 = [C1 D1*C2] [x2] + D1*D2 u2
  1142. a = np.vstack((np.hstack((self.A, np.dot(self.B, other.C))),
  1143. np.hstack((zeros((n2, n1)), other.A))))
  1144. b = np.vstack((np.dot(self.B, other.D), other.B))
  1145. c = np.hstack((self.C, np.dot(self.D, other.C)))
  1146. d = np.dot(self.D, other.D)
  1147. else:
  1148. # Assume that other is a scalar / matrix
  1149. # For post multiplication the input gets scaled
  1150. a = self.A
  1151. b = np.dot(self.B, other)
  1152. c = self.C
  1153. d = np.dot(self.D, other)
  1154. common_dtype = np.result_type(a.dtype, b.dtype, c.dtype, d.dtype)
  1155. return StateSpace(np.asarray(a, dtype=common_dtype),
  1156. np.asarray(b, dtype=common_dtype),
  1157. np.asarray(c, dtype=common_dtype),
  1158. np.asarray(d, dtype=common_dtype),
  1159. **self._dt_dict)
  1160. def __rmul__(self, other):
  1161. """Pre-multiply a scalar or matrix (but not StateSpace)"""
  1162. if not self._check_binop_other(other) or isinstance(other, StateSpace):
  1163. return NotImplemented
  1164. # For pre-multiplication only the output gets scaled
  1165. a = self.A
  1166. b = self.B
  1167. c = np.dot(other, self.C)
  1168. d = np.dot(other, self.D)
  1169. common_dtype = np.result_type(a.dtype, b.dtype, c.dtype, d.dtype)
  1170. return StateSpace(np.asarray(a, dtype=common_dtype),
  1171. np.asarray(b, dtype=common_dtype),
  1172. np.asarray(c, dtype=common_dtype),
  1173. np.asarray(d, dtype=common_dtype),
  1174. **self._dt_dict)
  1175. def __neg__(self):
  1176. """Negate the system (equivalent to pre-multiplying by -1)."""
  1177. return StateSpace(self.A, self.B, -self.C, -self.D, **self._dt_dict)
  1178. def __add__(self, other):
  1179. """
  1180. Adds two systems in the sense of frequency domain addition.
  1181. """
  1182. if not self._check_binop_other(other):
  1183. return NotImplemented
  1184. if isinstance(other, StateSpace):
  1185. # Disallow mix of discrete and continuous systems.
  1186. if type(other) is not type(self):
  1187. raise TypeError(f'Cannot add {type(self)} and {type(other)}')
  1188. if self.dt != other.dt:
  1189. raise TypeError('Cannot add systems with different `dt`.')
  1190. # Interconnection of systems
  1191. # x1' = A1 x1 + B1 u
  1192. # y1 = C1 x1 + D1 u
  1193. # x2' = A2 x2 + B2 u
  1194. # y2 = C2 x2 + D2 u
  1195. # y = y1 + y2
  1196. #
  1197. # Plugging in yields
  1198. # [x1'] [A1 0 ] [x1] [B1]
  1199. # [x2'] = [0 A2] [x2] + [B2] u
  1200. # [x1]
  1201. # y = [C1 C2] [x2] + [D1 + D2] u
  1202. a = linalg.block_diag(self.A, other.A)
  1203. b = np.vstack((self.B, other.B))
  1204. c = np.hstack((self.C, other.C))
  1205. d = self.D + other.D
  1206. else:
  1207. other = np.atleast_2d(other)
  1208. if self.D.shape == other.shape:
  1209. # A scalar/matrix is really just a static system (A=0, B=0, C=0)
  1210. a = self.A
  1211. b = self.B
  1212. c = self.C
  1213. d = self.D + other
  1214. else:
  1215. raise ValueError("Cannot add systems with incompatible "
  1216. f"dimensions ({self.D.shape} and {other.shape})")
  1217. common_dtype = np.result_type(a.dtype, b.dtype, c.dtype, d.dtype)
  1218. return StateSpace(np.asarray(a, dtype=common_dtype),
  1219. np.asarray(b, dtype=common_dtype),
  1220. np.asarray(c, dtype=common_dtype),
  1221. np.asarray(d, dtype=common_dtype),
  1222. **self._dt_dict)
  1223. def __sub__(self, other):
  1224. if not self._check_binop_other(other):
  1225. return NotImplemented
  1226. return self.__add__(-other)
  1227. def __radd__(self, other):
  1228. if not self._check_binop_other(other):
  1229. return NotImplemented
  1230. return self.__add__(other)
  1231. def __rsub__(self, other):
  1232. if not self._check_binop_other(other):
  1233. return NotImplemented
  1234. return (-self).__add__(other)
  1235. def __truediv__(self, other):
  1236. """
  1237. Divide by a scalar
  1238. """
  1239. # Division by non-StateSpace scalars
  1240. if not self._check_binop_other(other) or isinstance(other, StateSpace):
  1241. return NotImplemented
  1242. if isinstance(other, np.ndarray) and other.ndim > 0:
  1243. # It's ambiguous what this means, so disallow it
  1244. raise ValueError("Cannot divide StateSpace by non-scalar numpy arrays")
  1245. return self.__mul__(1/other)
  1246. @property
  1247. def A(self):
  1248. """State matrix of the `StateSpace` system."""
  1249. return self._A
  1250. @A.setter
  1251. def A(self, A):
  1252. self._A = np.atleast_2d(A) if A is not None else None
  1253. @property
  1254. def B(self):
  1255. """Input matrix of the `StateSpace` system."""
  1256. return self._B
  1257. @B.setter
  1258. def B(self, B):
  1259. self._B = np.atleast_2d(B) if B is not None else None
  1260. self.inputs = self.B.shape[-1]
  1261. @property
  1262. def C(self):
  1263. """Output matrix of the `StateSpace` system."""
  1264. return self._C
  1265. @C.setter
  1266. def C(self, C):
  1267. self._C = np.atleast_2d(C) if C is not None else None
  1268. self.outputs = self.C.shape[0]
  1269. @property
  1270. def D(self):
  1271. """Feedthrough matrix of the `StateSpace` system."""
  1272. return self._D
  1273. @D.setter
  1274. def D(self, D):
  1275. self._D = np.atleast_2d(D) if D is not None else None
  1276. def _copy(self, system):
  1277. """
  1278. Copy the parameters of another `StateSpace` system.
  1279. Parameters
  1280. ----------
  1281. system : instance of `StateSpace`
  1282. The state-space system that is to be copied
  1283. """
  1284. self.A = system.A
  1285. self.B = system.B
  1286. self.C = system.C
  1287. self.D = system.D
  1288. def to_tf(self, **kwargs):
  1289. """
  1290. Convert system representation to `TransferFunction`.
  1291. Parameters
  1292. ----------
  1293. kwargs : dict, optional
  1294. Additional keywords passed to `ss2zpk`
  1295. Returns
  1296. -------
  1297. sys : instance of `TransferFunction`
  1298. Transfer function of the current system
  1299. """
  1300. return TransferFunction(*ss2tf(self._A, self._B, self._C, self._D,
  1301. **kwargs), **self._dt_dict)
  1302. def to_zpk(self, **kwargs):
  1303. """
  1304. Convert system representation to `ZerosPolesGain`.
  1305. Parameters
  1306. ----------
  1307. kwargs : dict, optional
  1308. Additional keywords passed to `ss2zpk`
  1309. Returns
  1310. -------
  1311. sys : instance of `ZerosPolesGain`
  1312. Zeros, poles, gain representation of the current system
  1313. """
  1314. return ZerosPolesGain(*ss2zpk(self._A, self._B, self._C, self._D,
  1315. **kwargs), **self._dt_dict)
  1316. def to_ss(self):
  1317. """
  1318. Return a copy of the current `StateSpace` system.
  1319. Returns
  1320. -------
  1321. sys : instance of `StateSpace`
  1322. The current system (copy)
  1323. """
  1324. return copy.deepcopy(self)
  1325. class StateSpaceContinuous(StateSpace, lti):
  1326. r"""
  1327. Continuous-time Linear Time Invariant system in state-space form.
  1328. Represents the system as the continuous-time, first order differential
  1329. equation :math:`\dot{x} = A x + B u`.
  1330. Continuous-time `StateSpace` systems inherit additional functionality
  1331. from the `lti` class.
  1332. Parameters
  1333. ----------
  1334. *system: arguments
  1335. The `StateSpace` class can be instantiated with 1 or 3 arguments.
  1336. The following gives the number of input arguments and their
  1337. interpretation:
  1338. * 1: `lti` system: (`StateSpace`, `TransferFunction` or
  1339. `ZerosPolesGain`)
  1340. * 4: array_like: (A, B, C, D)
  1341. See Also
  1342. --------
  1343. TransferFunction, ZerosPolesGain, lti
  1344. ss2zpk, ss2tf, zpk2sos
  1345. Notes
  1346. -----
  1347. Changing the value of properties that are not part of the
  1348. `StateSpace` system representation (such as `zeros` or `poles`) is very
  1349. inefficient and may lead to numerical inaccuracies. It is better to
  1350. convert to the specific system representation first. For example, call
  1351. ``sys = sys.to_zpk()`` before accessing/changing the zeros, poles or gain.
  1352. Examples
  1353. --------
  1354. >>> import numpy as np
  1355. >>> from scipy import signal
  1356. >>> a = np.array([[0, 1], [0, 0]])
  1357. >>> b = np.array([[0], [1]])
  1358. >>> c = np.array([[1, 0]])
  1359. >>> d = np.array([[0]])
  1360. >>> sys = signal.StateSpace(a, b, c, d)
  1361. >>> print(sys)
  1362. StateSpaceContinuous(
  1363. array([[0, 1],
  1364. [0, 0]]),
  1365. array([[0],
  1366. [1]]),
  1367. array([[1, 0]]),
  1368. array([[0]]),
  1369. dt: None
  1370. )
  1371. """
  1372. def to_discrete(self, dt, method='zoh', alpha=None):
  1373. """
  1374. Returns the discretized `StateSpace` system.
  1375. Parameters: See `cont2discrete` for details.
  1376. Returns
  1377. -------
  1378. sys: instance of `dlti` and `StateSpace`
  1379. """
  1380. return StateSpace(*cont2discrete((self.A, self.B, self.C, self.D),
  1381. dt,
  1382. method=method,
  1383. alpha=alpha)[:-1],
  1384. dt=dt)
  1385. class StateSpaceDiscrete(StateSpace, dlti):
  1386. r"""
  1387. Discrete-time Linear Time Invariant system in state-space form.
  1388. Represents the system as the discrete-time difference equation
  1389. :math:`x[k+1] = A x[k] + B u[k]`.
  1390. `StateSpace` systems inherit additional functionality from the `dlti`
  1391. class.
  1392. Parameters
  1393. ----------
  1394. *system: arguments
  1395. The `StateSpace` class can be instantiated with 1 or 3 arguments.
  1396. The following gives the number of input arguments and their
  1397. interpretation:
  1398. * 1: `dlti` system: (`StateSpace`, `TransferFunction` or
  1399. `ZerosPolesGain`)
  1400. * 4: array_like: (A, B, C, D)
  1401. dt: float, optional
  1402. Sampling time [s] of the discrete-time systems. Defaults to `True`
  1403. (unspecified sampling time). Must be specified as a keyword argument,
  1404. for example, ``dt=0.1``.
  1405. See Also
  1406. --------
  1407. TransferFunction, ZerosPolesGain, dlti
  1408. ss2zpk, ss2tf, zpk2sos
  1409. Notes
  1410. -----
  1411. Changing the value of properties that are not part of the
  1412. `StateSpace` system representation (such as `zeros` or `poles`) is very
  1413. inefficient and may lead to numerical inaccuracies. It is better to
  1414. convert to the specific system representation first. For example, call
  1415. ``sys = sys.to_zpk()`` before accessing/changing the zeros, poles or gain.
  1416. Examples
  1417. --------
  1418. >>> import numpy as np
  1419. >>> from scipy import signal
  1420. >>> a = np.array([[1, 0.1], [0, 1]])
  1421. >>> b = np.array([[0.005], [0.1]])
  1422. >>> c = np.array([[1, 0]])
  1423. >>> d = np.array([[0]])
  1424. >>> signal.StateSpace(a, b, c, d, dt=0.1)
  1425. StateSpaceDiscrete(
  1426. array([[ 1. , 0.1],
  1427. [ 0. , 1. ]]),
  1428. array([[ 0.005],
  1429. [ 0.1 ]]),
  1430. array([[1, 0]]),
  1431. array([[0]]),
  1432. dt: 0.1
  1433. )
  1434. """
  1435. pass
  1436. def lsim(system, U, T, X0=None, interp=True):
  1437. """
  1438. Simulate output of a continuous-time linear system.
  1439. Parameters
  1440. ----------
  1441. system : an instance of the LTI class or a tuple describing the system.
  1442. The following gives the number of elements in the tuple and
  1443. the interpretation:
  1444. * 1: (instance of `lti`)
  1445. * 2: (num, den)
  1446. * 3: (zeros, poles, gain)
  1447. * 4: (A, B, C, D)
  1448. U : array_like
  1449. An input array describing the input at each time `T`
  1450. (interpolation is assumed between given times). If there are
  1451. multiple inputs, then each column of the rank-2 array
  1452. represents an input. If U = 0 or None, a zero input is used.
  1453. T : array_like
  1454. The time steps at which the input is defined and at which the
  1455. output is desired. Must be nonnegative, increasing, and equally spaced.
  1456. X0 : array_like, optional
  1457. The initial conditions on the state vector (zero by default).
  1458. interp : bool, optional
  1459. Whether to use linear (True, the default) or zero-order-hold (False)
  1460. interpolation for the input array.
  1461. Returns
  1462. -------
  1463. T : 1D ndarray
  1464. Time values for the output.
  1465. yout : 1D ndarray
  1466. System response.
  1467. xout : ndarray
  1468. Time evolution of the state vector.
  1469. Notes
  1470. -----
  1471. If (num, den) is passed in for ``system``, coefficients for both the
  1472. numerator and denominator should be specified in descending exponent
  1473. order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
  1474. Examples
  1475. --------
  1476. We'll use `lsim` to simulate an analog Bessel filter applied to
  1477. a signal.
  1478. >>> import numpy as np
  1479. >>> from scipy.signal import bessel, lsim
  1480. >>> import matplotlib.pyplot as plt
  1481. Create a low-pass Bessel filter with a cutoff of 12 Hz.
  1482. >>> b, a = bessel(N=5, Wn=2*np.pi*12, btype='lowpass', analog=True)
  1483. Generate data to which the filter is applied.
  1484. >>> t = np.linspace(0, 1.25, 500, endpoint=False)
  1485. The input signal is the sum of three sinusoidal curves, with
  1486. frequencies 4 Hz, 40 Hz, and 80 Hz. The filter should mostly
  1487. eliminate the 40 Hz and 80 Hz components, leaving just the 4 Hz signal.
  1488. >>> u = (np.cos(2*np.pi*4*t) + 0.6*np.sin(2*np.pi*40*t) +
  1489. ... 0.5*np.cos(2*np.pi*80*t))
  1490. Simulate the filter with `lsim`.
  1491. >>> tout, yout, xout = lsim((b, a), U=u, T=t)
  1492. Plot the result.
  1493. >>> plt.plot(t, u, 'r', alpha=0.5, linewidth=1, label='input')
  1494. >>> plt.plot(tout, yout, 'k', linewidth=1.5, label='output')
  1495. >>> plt.legend(loc='best', shadow=True, framealpha=1)
  1496. >>> plt.grid(alpha=0.3)
  1497. >>> plt.xlabel('t')
  1498. >>> plt.show()
  1499. In a second example, we simulate a double integrator ``y'' = u``, with
  1500. a constant input ``u = 1``. We'll use the state space representation
  1501. of the integrator.
  1502. >>> from scipy.signal import lti
  1503. >>> A = np.array([[0.0, 1.0], [0.0, 0.0]])
  1504. >>> B = np.array([[0.0], [1.0]])
  1505. >>> C = np.array([[1.0, 0.0]])
  1506. >>> D = 0.0
  1507. >>> system = lti(A, B, C, D)
  1508. `t` and `u` define the time and input signal for the system to
  1509. be simulated.
  1510. >>> t = np.linspace(0, 5, num=50)
  1511. >>> u = np.ones_like(t)
  1512. Compute the simulation, and then plot `y`. As expected, the plot shows
  1513. the curve ``y = 0.5*t**2``.
  1514. >>> tout, y, x = lsim(system, u, t)
  1515. >>> plt.plot(t, y)
  1516. >>> plt.grid(alpha=0.3)
  1517. >>> plt.xlabel('t')
  1518. >>> plt.show()
  1519. """
  1520. if isinstance(system, lti):
  1521. sys = system._as_ss()
  1522. elif isinstance(system, dlti):
  1523. raise AttributeError('lsim can only be used with continuous-time '
  1524. 'systems.')
  1525. else:
  1526. sys = lti(*system)._as_ss()
  1527. T = atleast_1d(T)
  1528. if len(T.shape) != 1:
  1529. raise ValueError("T must be a rank-1 array.")
  1530. A, B, C, D = map(np.asarray, (sys.A, sys.B, sys.C, sys.D))
  1531. n_states = A.shape[0]
  1532. n_inputs = B.shape[1]
  1533. n_steps = T.size
  1534. if X0 is None:
  1535. X0 = zeros(n_states, sys.A.dtype)
  1536. xout = np.empty((n_steps, n_states), sys.A.dtype)
  1537. if T[0] == 0:
  1538. xout[0] = X0
  1539. elif T[0] > 0:
  1540. # step forward to initial time, with zero input
  1541. xout[0] = dot(X0, linalg.expm(transpose(A) * T[0]))
  1542. else:
  1543. raise ValueError("Initial time must be nonnegative")
  1544. no_input = (U is None or
  1545. (isinstance(U, int | float) and U == 0.) or
  1546. not np.any(U))
  1547. if n_steps == 1:
  1548. yout = squeeze(xout @ C.T)
  1549. if not no_input:
  1550. yout += squeeze(U @ D.T)
  1551. return T, yout, squeeze(xout)
  1552. dt = T[1] - T[0]
  1553. if not np.allclose(np.diff(T), dt):
  1554. raise ValueError("Time steps are not equally spaced.")
  1555. if no_input:
  1556. # Zero input: just use matrix exponential
  1557. # take transpose because state is a row vector
  1558. expAT_dt = linalg.expm(A.T * dt)
  1559. for i in range(1, n_steps):
  1560. xout[i] = xout[i-1] @ expAT_dt
  1561. yout = squeeze(xout @ C.T)
  1562. return T, yout, squeeze(xout)
  1563. # Nonzero input
  1564. U = atleast_1d(U)
  1565. if U.ndim == 1:
  1566. U = U[:, np.newaxis]
  1567. if U.shape[0] != n_steps:
  1568. raise ValueError("U must have the same number of rows "
  1569. "as elements in T.")
  1570. if U.shape[1] != n_inputs:
  1571. raise ValueError("System does not define that many inputs.")
  1572. if not interp:
  1573. # Zero-order hold
  1574. # Algorithm: to integrate from time 0 to time dt, we solve
  1575. # xdot = A x + B u, x(0) = x0
  1576. # udot = 0, u(0) = u0.
  1577. #
  1578. # Solution is
  1579. # [ x(dt) ] [ A*dt B*dt ] [ x0 ]
  1580. # [ u(dt) ] = exp [ 0 0 ] [ u0 ]
  1581. M = np.vstack([np.hstack([A * dt, B * dt]),
  1582. np.zeros((n_inputs, n_states + n_inputs))])
  1583. # transpose everything because the state and input are row vectors
  1584. expMT = linalg.expm(M.T)
  1585. Ad = expMT[:n_states, :n_states]
  1586. Bd = expMT[n_states:, :n_states]
  1587. for i in range(1, n_steps):
  1588. xout[i] = xout[i-1] @ Ad + U[i-1] @ Bd
  1589. else:
  1590. # Linear interpolation between steps
  1591. # Algorithm: to integrate from time 0 to time dt, with linear
  1592. # interpolation between inputs u(0) = u0 and u(dt) = u1, we solve
  1593. # xdot = A x + B u, x(0) = x0
  1594. # udot = (u1 - u0) / dt, u(0) = u0.
  1595. #
  1596. # Solution is
  1597. # [ x(dt) ] [ A*dt B*dt 0 ] [ x0 ]
  1598. # [ u(dt) ] = exp [ 0 0 I ] [ u0 ]
  1599. # [u1 - u0] [ 0 0 0 ] [u1 - u0]
  1600. M = np.vstack([np.hstack([A * dt, B * dt,
  1601. np.zeros((n_states, n_inputs))]),
  1602. np.hstack([np.zeros((n_inputs, n_states + n_inputs)),
  1603. np.identity(n_inputs)]),
  1604. np.zeros((n_inputs, n_states + 2 * n_inputs))])
  1605. expMT = linalg.expm(M.T)
  1606. Ad = expMT[:n_states, :n_states]
  1607. Bd1 = expMT[n_states+n_inputs:, :n_states]
  1608. Bd0 = expMT[n_states:n_states + n_inputs, :n_states] - Bd1
  1609. for i in range(1, n_steps):
  1610. xout[i] = xout[i-1] @ Ad + U[i-1] @ Bd0 + U[i] @ Bd1
  1611. yout = squeeze(xout @ C.T) + squeeze(U @ D.T)
  1612. return T, yout, squeeze(xout)
  1613. def _default_response_times(A, n):
  1614. """Compute a reasonable set of time samples for the response time.
  1615. This function is used by `impulse` and `step` to compute the response time
  1616. when the `T` argument to the function is None.
  1617. Parameters
  1618. ----------
  1619. A : array_like
  1620. The system matrix, which is square.
  1621. n : int
  1622. The number of time samples to generate.
  1623. Returns
  1624. -------
  1625. t : ndarray
  1626. The 1-D array of length `n` of time samples at which the response
  1627. is to be computed.
  1628. """
  1629. # Create a reasonable time interval.
  1630. # TODO: This could use some more work.
  1631. # For example, what is expected when the system is unstable?
  1632. vals = linalg.eigvals(A)
  1633. r = min(abs(real(vals)))
  1634. if r == 0.0:
  1635. r = 1.0
  1636. tc = 1.0 / r
  1637. t = linspace(0.0, 7 * tc, n)
  1638. return t
  1639. def impulse(system, X0=None, T=None, N=None):
  1640. """Impulse response of continuous-time system.
  1641. Parameters
  1642. ----------
  1643. system : an instance of the LTI class or a tuple of array_like
  1644. describing the system.
  1645. The following gives the number of elements in the tuple and
  1646. the interpretation:
  1647. * 1 (instance of `lti`)
  1648. * 2 (num, den)
  1649. * 3 (zeros, poles, gain)
  1650. * 4 (A, B, C, D)
  1651. X0 : array_like, optional
  1652. Initial state-vector. Defaults to zero.
  1653. T : array_like, optional
  1654. Time points. Computed if not given.
  1655. N : int, optional
  1656. The number of time points to compute (if `T` is not given).
  1657. Returns
  1658. -------
  1659. T : ndarray
  1660. A 1-D array of time points.
  1661. yout : ndarray
  1662. A 1-D array containing the impulse response of the system (except for
  1663. singularities at zero).
  1664. Notes
  1665. -----
  1666. If (num, den) is passed in for ``system``, coefficients for both the
  1667. numerator and denominator should be specified in descending exponent
  1668. order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
  1669. Examples
  1670. --------
  1671. Compute the impulse response of a second order system with a repeated
  1672. root: ``x''(t) + 2*x'(t) + x(t) = u(t)``
  1673. >>> from scipy import signal
  1674. >>> system = ([1.0], [1.0, 2.0, 1.0])
  1675. >>> t, y = signal.impulse(system)
  1676. >>> import matplotlib.pyplot as plt
  1677. >>> plt.plot(t, y)
  1678. """
  1679. if isinstance(system, lti):
  1680. sys = system._as_ss()
  1681. elif isinstance(system, dlti):
  1682. raise AttributeError('impulse can only be used with continuous-time '
  1683. 'systems.')
  1684. else:
  1685. sys = lti(*system)._as_ss()
  1686. if X0 is None:
  1687. X = squeeze(sys.B)
  1688. else:
  1689. X = squeeze(sys.B + X0)
  1690. if N is None:
  1691. N = 100
  1692. if T is None:
  1693. T = _default_response_times(sys.A, N)
  1694. else:
  1695. T = asarray(T)
  1696. _, h, _ = lsim(sys, 0., T, X, interp=False)
  1697. return T, h
  1698. def step(system, X0=None, T=None, N=None):
  1699. """Step response of continuous-time system.
  1700. Parameters
  1701. ----------
  1702. system : an instance of the LTI class or a tuple of array_like
  1703. describing the system.
  1704. The following gives the number of elements in the tuple and
  1705. the interpretation:
  1706. * 1 (instance of `lti`)
  1707. * 2 (num, den)
  1708. * 3 (zeros, poles, gain)
  1709. * 4 (A, B, C, D)
  1710. X0 : array_like, optional
  1711. Initial state-vector (default is zero).
  1712. T : array_like, optional
  1713. Time points (computed if not given).
  1714. N : int, optional
  1715. Number of time points to compute if `T` is not given.
  1716. Returns
  1717. -------
  1718. T : 1D ndarray
  1719. Output time points.
  1720. yout : 1D ndarray
  1721. Step response of system.
  1722. Notes
  1723. -----
  1724. If (num, den) is passed in for ``system``, coefficients for both the
  1725. numerator and denominator should be specified in descending exponent
  1726. order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
  1727. Examples
  1728. --------
  1729. >>> from scipy import signal
  1730. >>> import matplotlib.pyplot as plt
  1731. >>> lti = signal.lti([1.0], [1.0, 1.0])
  1732. >>> t, y = signal.step(lti)
  1733. >>> plt.plot(t, y)
  1734. >>> plt.xlabel('Time [s]')
  1735. >>> plt.ylabel('Amplitude')
  1736. >>> plt.title('Step response for 1. Order Lowpass')
  1737. >>> plt.grid()
  1738. """
  1739. if isinstance(system, lti):
  1740. sys = system._as_ss()
  1741. elif isinstance(system, dlti):
  1742. raise AttributeError('step can only be used with continuous-time '
  1743. 'systems.')
  1744. else:
  1745. sys = lti(*system)._as_ss()
  1746. if N is None:
  1747. N = 100
  1748. if T is None:
  1749. T = _default_response_times(sys.A, N)
  1750. else:
  1751. T = asarray(T)
  1752. U = ones(T.shape, sys.A.dtype)
  1753. vals = lsim(sys, U, T, X0=X0, interp=False)
  1754. return vals[0], vals[1]
  1755. def bode(system, w=None, n=100):
  1756. """
  1757. Calculate Bode magnitude and phase data of a continuous-time system.
  1758. Parameters
  1759. ----------
  1760. system : an instance of the LTI class or a tuple describing the system.
  1761. The following gives the number of elements in the tuple and
  1762. the interpretation:
  1763. * 1 (instance of `lti`)
  1764. * 2 (num, den)
  1765. * 3 (zeros, poles, gain)
  1766. * 4 (A, B, C, D)
  1767. w : array_like, optional
  1768. Array of frequencies (in rad/s). Magnitude and phase data is calculated
  1769. for every value in this array. If not given a reasonable set will be
  1770. calculated.
  1771. n : int, optional
  1772. Number of frequency points to compute if `w` is not given. The `n`
  1773. frequencies are logarithmically spaced in an interval chosen to
  1774. include the influence of the poles and zeros of the system.
  1775. Returns
  1776. -------
  1777. w : 1D ndarray
  1778. Frequency array [rad/s]
  1779. mag : 1D ndarray
  1780. Magnitude array [dB]
  1781. phase : 1D ndarray
  1782. Phase array [deg]
  1783. Notes
  1784. -----
  1785. If (num, den) is passed in for ``system``, coefficients for both the
  1786. numerator and denominator should be specified in descending exponent
  1787. order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
  1788. .. versionadded:: 0.11.0
  1789. Examples
  1790. --------
  1791. >>> from scipy import signal
  1792. >>> import matplotlib.pyplot as plt
  1793. >>> sys = signal.TransferFunction([1], [1, 1])
  1794. >>> w, mag, phase = signal.bode(sys)
  1795. >>> plt.figure()
  1796. >>> plt.semilogx(w, mag) # Bode magnitude plot
  1797. >>> plt.figure()
  1798. >>> plt.semilogx(w, phase) # Bode phase plot
  1799. >>> plt.show()
  1800. """
  1801. w, y = freqresp(system, w=w, n=n)
  1802. mag = 20.0 * np.log10(abs(y))
  1803. phase = np.unwrap(np.arctan2(y.imag, y.real)) * 180.0 / np.pi
  1804. return w, mag, phase
  1805. def freqresp(system, w=None, n=10000):
  1806. r"""Calculate the frequency response of a continuous-time system.
  1807. Parameters
  1808. ----------
  1809. system : an instance of the `lti` class or a tuple describing the system.
  1810. The following gives the number of elements in the tuple and
  1811. the interpretation:
  1812. * 1 (instance of `lti`)
  1813. * 2 (num, den)
  1814. * 3 (zeros, poles, gain)
  1815. * 4 (A, B, C, D)
  1816. w : array_like, optional
  1817. Array of frequencies (in rad/s). Magnitude and phase data is
  1818. calculated for every value in this array. If not given, a reasonable
  1819. set will be calculated.
  1820. n : int, optional
  1821. Number of frequency points to compute if `w` is not given. The `n`
  1822. frequencies are logarithmically spaced in an interval chosen to
  1823. include the influence of the poles and zeros of the system.
  1824. Returns
  1825. -------
  1826. w : 1D ndarray
  1827. Frequency array [rad/s]
  1828. H : 1D ndarray
  1829. Array of complex magnitude values
  1830. Notes
  1831. -----
  1832. If (num, den) is passed in for ``system``, coefficients for both the
  1833. numerator and denominator should be specified in descending exponent
  1834. order (e.g. ``s^2 + 3s + 5`` would be represented as ``[1, 3, 5]``).
  1835. Examples
  1836. --------
  1837. Generating the Nyquist plot of a transfer function
  1838. >>> from scipy import signal
  1839. >>> import matplotlib.pyplot as plt
  1840. Construct the transfer function :math:`H(s) = \frac{5}{(s-1)^3}`:
  1841. >>> s1 = signal.ZerosPolesGain([], [1, 1, 1], [5])
  1842. >>> w, H = signal.freqresp(s1)
  1843. >>> plt.figure()
  1844. >>> plt.plot(H.real, H.imag, "b")
  1845. >>> plt.plot(H.real, -H.imag, "r")
  1846. >>> plt.show()
  1847. """
  1848. if isinstance(system, lti):
  1849. if isinstance(system, TransferFunction | ZerosPolesGain):
  1850. sys = system
  1851. else:
  1852. sys = system._as_zpk()
  1853. elif isinstance(system, dlti):
  1854. raise AttributeError('freqresp can only be used with continuous-time '
  1855. 'systems.')
  1856. else:
  1857. sys = lti(*system)._as_zpk()
  1858. if sys.inputs != 1 or sys.outputs != 1:
  1859. raise ValueError("freqresp() requires a SISO (single input, single "
  1860. "output) system.")
  1861. if w is not None:
  1862. worN = w
  1863. else:
  1864. worN = n
  1865. if isinstance(sys, TransferFunction):
  1866. # In the call to freqs(), sys.num.ravel() is used because there are
  1867. # cases where sys.num is a 2-D array with a single row.
  1868. w, h = freqs(sys.num.ravel(), sys.den, worN=worN)
  1869. elif isinstance(sys, ZerosPolesGain):
  1870. w, h = freqs_zpk(sys.zeros, sys.poles, sys.gain, worN=worN)
  1871. return w, h
  1872. # This class will be used by place_poles to return its results
  1873. # see https://code.activestate.com/recipes/52308/
  1874. class Bunch:
  1875. def __init__(self, **kwds):
  1876. self.__dict__.update(kwds)
  1877. def _valid_inputs(A, B, poles, method, rtol, maxiter):
  1878. """
  1879. Check the poles come in complex conjugate pairs
  1880. Check shapes of A, B and poles are compatible.
  1881. Check the method chosen is compatible with provided poles
  1882. Return update method to use and ordered poles
  1883. """
  1884. poles = np.asarray(poles)
  1885. if poles.ndim > 1:
  1886. raise ValueError("Poles must be a 1D array like.")
  1887. # Will raise ValueError if poles do not come in complex conjugates pairs
  1888. poles = _order_complex_poles(poles)
  1889. if A.ndim > 2:
  1890. raise ValueError("A must be a 2D array/matrix.")
  1891. if B.ndim > 2:
  1892. raise ValueError("B must be a 2D array/matrix")
  1893. if A.shape[0] != A.shape[1]:
  1894. raise ValueError("A must be square")
  1895. if len(poles) > A.shape[0]:
  1896. raise ValueError(
  1897. f"maximum number of poles is {A.shape[0]} but you asked for {len(poles)}"
  1898. )
  1899. if len(poles) < A.shape[0]:
  1900. raise ValueError(
  1901. f"number of poles is {len(poles)} but you should provide {A.shape[0]}"
  1902. )
  1903. r = np.linalg.matrix_rank(B)
  1904. for p in poles:
  1905. if sum(p == poles) > r:
  1906. raise ValueError("at least one of the requested pole is repeated "
  1907. "more than rank(B) times")
  1908. # Choose update method
  1909. update_loop = _YT_loop
  1910. if method not in ('KNV0','YT'):
  1911. raise ValueError("The method keyword must be one of 'YT' or 'KNV0'")
  1912. if method == "KNV0":
  1913. update_loop = _KNV0_loop
  1914. if not all(np.isreal(poles)):
  1915. raise ValueError("Complex poles are not supported by KNV0")
  1916. if maxiter < 1:
  1917. raise ValueError("maxiter must be at least equal to 1")
  1918. # We do not check rtol <= 0 as the user can use a negative rtol to
  1919. # force maxiter iterations
  1920. if rtol > 1:
  1921. raise ValueError("rtol can not be greater than 1")
  1922. return update_loop, poles
  1923. def _order_complex_poles(poles):
  1924. """
  1925. Check we have complex conjugates pairs and reorder P according to YT, ie
  1926. real_poles, complex_i, conjugate complex_i, ....
  1927. The lexicographic sort on the complex poles is added to help the user to
  1928. compare sets of poles.
  1929. """
  1930. ordered_poles = np.sort(poles[np.isreal(poles)])
  1931. im_poles = []
  1932. for p in np.sort(poles[np.imag(poles) < 0]):
  1933. if np.conj(p) in poles:
  1934. im_poles.extend((p, np.conj(p)))
  1935. ordered_poles = np.hstack((ordered_poles, im_poles))
  1936. if poles.shape[0] != len(ordered_poles):
  1937. raise ValueError("Complex poles must come with their conjugates")
  1938. return ordered_poles
  1939. def _KNV0(B, ker_pole, transfer_matrix, j, poles):
  1940. """
  1941. Algorithm "KNV0" Kautsky et Al. Robust pole
  1942. assignment in linear state feedback, Int journal of Control
  1943. 1985, vol 41 p 1129->1155
  1944. https://la.epfl.ch/files/content/sites/la/files/
  1945. users/105941/public/KautskyNicholsDooren
  1946. """
  1947. # Remove xj form the base
  1948. transfer_matrix_not_j = np.delete(transfer_matrix, j, axis=1)
  1949. # If we QR this matrix in full mode Q=Q0|Q1
  1950. # then Q1 will be a single column orthogonal to
  1951. # Q0, that's what we are looking for !
  1952. # After merge of gh-4249 great speed improvements could be achieved
  1953. # using QR updates instead of full QR in the line below
  1954. # To debug with numpy qr uncomment the line below
  1955. # Q, R = np.linalg.qr(transfer_matrix_not_j, mode="complete")
  1956. Q, R = s_qr(transfer_matrix_not_j, mode="full")
  1957. mat_ker_pj = np.dot(ker_pole[j], ker_pole[j].T)
  1958. yj = np.dot(mat_ker_pj, Q[:, -1])
  1959. # If Q[:, -1] is "almost" orthogonal to ker_pole[j] its
  1960. # projection into ker_pole[j] will yield a vector
  1961. # close to 0. As we are looking for a vector in ker_pole[j]
  1962. # simply stick with transfer_matrix[:, j] (unless someone provides me with
  1963. # a better choice ?)
  1964. if not np.allclose(yj, 0):
  1965. xj = yj/np.linalg.norm(yj)
  1966. transfer_matrix[:, j] = xj
  1967. # KNV does not support complex poles, using YT technique the two lines
  1968. # below seem to work 9 out of 10 times but it is not reliable enough:
  1969. # transfer_matrix[:, j]=real(xj)
  1970. # transfer_matrix[:, j+1]=imag(xj)
  1971. # Add this at the beginning of this function if you wish to test
  1972. # complex support:
  1973. # if ~np.isreal(P[j]) and (j>=B.shape[0]-1 or P[j]!=np.conj(P[j+1])):
  1974. # return
  1975. # Problems arise when imag(xj)=>0 I have no idea on how to fix this
  1976. def _YT_real(ker_pole, Q, transfer_matrix, i, j):
  1977. """
  1978. Applies algorithm from YT section 6.1 page 19 related to real pairs
  1979. """
  1980. # step 1 page 19
  1981. u = Q[:, -2, np.newaxis]
  1982. v = Q[:, -1, np.newaxis]
  1983. # step 2 page 19
  1984. m = np.dot(np.dot(ker_pole[i].T, np.dot(u, v.T) -
  1985. np.dot(v, u.T)), ker_pole[j])
  1986. # step 3 page 19
  1987. um, sm, vm = np.linalg.svd(m)
  1988. # mu1, mu2 two first columns of U => 2 first lines of U.T
  1989. mu1, mu2 = um.T[:2, :, np.newaxis]
  1990. # VM is V.T with numpy we want the first two lines of V.T
  1991. nu1, nu2 = vm[:2, :, np.newaxis]
  1992. # what follows is a rough python translation of the formulas
  1993. # in section 6.2 page 20 (step 4)
  1994. transfer_matrix_j_mo_transfer_matrix_j = np.vstack((
  1995. transfer_matrix[:, i, np.newaxis],
  1996. transfer_matrix[:, j, np.newaxis]))
  1997. if not np.allclose(sm[0], sm[1]):
  1998. ker_pole_imo_mu1 = np.dot(ker_pole[i], mu1)
  1999. ker_pole_i_nu1 = np.dot(ker_pole[j], nu1)
  2000. ker_pole_mu_nu = np.vstack((ker_pole_imo_mu1, ker_pole_i_nu1))
  2001. else:
  2002. ker_pole_ij = np.vstack((
  2003. np.hstack((ker_pole[i],
  2004. np.zeros(ker_pole[i].shape))),
  2005. np.hstack((np.zeros(ker_pole[j].shape),
  2006. ker_pole[j]))
  2007. ))
  2008. mu_nu_matrix = np.vstack(
  2009. (np.hstack((mu1, mu2)), np.hstack((nu1, nu2)))
  2010. )
  2011. ker_pole_mu_nu = np.dot(ker_pole_ij, mu_nu_matrix)
  2012. transfer_matrix_ij = np.dot(np.dot(ker_pole_mu_nu, ker_pole_mu_nu.T),
  2013. transfer_matrix_j_mo_transfer_matrix_j)
  2014. if not np.allclose(transfer_matrix_ij, 0):
  2015. transfer_matrix_ij = (np.sqrt(2)*transfer_matrix_ij /
  2016. np.linalg.norm(transfer_matrix_ij))
  2017. transfer_matrix[:, i] = transfer_matrix_ij[
  2018. :transfer_matrix[:, i].shape[0], 0
  2019. ]
  2020. transfer_matrix[:, j] = transfer_matrix_ij[
  2021. transfer_matrix[:, i].shape[0]:, 0
  2022. ]
  2023. else:
  2024. # As in knv0 if transfer_matrix_j_mo_transfer_matrix_j is orthogonal to
  2025. # Vect{ker_pole_mu_nu} assign transfer_matrixi/transfer_matrix_j to
  2026. # ker_pole_mu_nu and iterate. As we are looking for a vector in
  2027. # Vect{Matker_pole_MU_NU} (see section 6.1 page 19) this might help
  2028. # (that's a guess, not a claim !)
  2029. transfer_matrix[:, i] = ker_pole_mu_nu[
  2030. :transfer_matrix[:, i].shape[0], 0
  2031. ]
  2032. transfer_matrix[:, j] = ker_pole_mu_nu[
  2033. transfer_matrix[:, i].shape[0]:, 0
  2034. ]
  2035. def _YT_complex(ker_pole, Q, transfer_matrix, i, j):
  2036. """
  2037. Applies algorithm from YT section 6.2 page 20 related to complex pairs
  2038. """
  2039. # step 1 page 20
  2040. ur = np.sqrt(2)*Q[:, -2, np.newaxis]
  2041. ui = np.sqrt(2)*Q[:, -1, np.newaxis]
  2042. u = ur + 1j*ui
  2043. # step 2 page 20
  2044. ker_pole_ij = ker_pole[i]
  2045. m = np.dot(np.dot(np.conj(ker_pole_ij.T), np.dot(u, np.conj(u).T) -
  2046. np.dot(np.conj(u), u.T)), ker_pole_ij)
  2047. # step 3 page 20
  2048. e_val, e_vec = np.linalg.eig(m)
  2049. # sort eigenvalues according to their module
  2050. e_val_idx = np.argsort(np.abs(e_val))
  2051. mu1 = e_vec[:, e_val_idx[-1], np.newaxis]
  2052. mu2 = e_vec[:, e_val_idx[-2], np.newaxis]
  2053. # what follows is a rough python translation of the formulas
  2054. # in section 6.2 page 20 (step 4)
  2055. # remember transfer_matrix_i has been split as
  2056. # transfer_matrix[i]=real(transfer_matrix_i) and
  2057. # transfer_matrix[j]=imag(transfer_matrix_i)
  2058. transfer_matrix_j_mo_transfer_matrix_j = (
  2059. transfer_matrix[:, i, np.newaxis] +
  2060. 1j*transfer_matrix[:, j, np.newaxis]
  2061. )
  2062. if not np.allclose(np.abs(e_val[e_val_idx[-1]]),
  2063. np.abs(e_val[e_val_idx[-2]])):
  2064. ker_pole_mu = np.dot(ker_pole_ij, mu1)
  2065. else:
  2066. mu1_mu2_matrix = np.hstack((mu1, mu2))
  2067. ker_pole_mu = np.dot(ker_pole_ij, mu1_mu2_matrix)
  2068. transfer_matrix_i_j = np.dot(np.dot(ker_pole_mu, np.conj(ker_pole_mu.T)),
  2069. transfer_matrix_j_mo_transfer_matrix_j)
  2070. if not np.allclose(transfer_matrix_i_j, 0):
  2071. transfer_matrix_i_j = (transfer_matrix_i_j /
  2072. np.linalg.norm(transfer_matrix_i_j))
  2073. transfer_matrix[:, i] = np.real(transfer_matrix_i_j[:, 0])
  2074. transfer_matrix[:, j] = np.imag(transfer_matrix_i_j[:, 0])
  2075. else:
  2076. # same idea as in YT_real
  2077. transfer_matrix[:, i] = np.real(ker_pole_mu[:, 0])
  2078. transfer_matrix[:, j] = np.imag(ker_pole_mu[:, 0])
  2079. def _YT_loop(ker_pole, transfer_matrix, poles, B, maxiter, rtol):
  2080. """
  2081. Algorithm "YT" Tits, Yang. Globally Convergent
  2082. Algorithms for Robust Pole Assignment by State Feedback
  2083. https://hdl.handle.net/1903/5598
  2084. The poles P have to be sorted accordingly to section 6.2 page 20
  2085. """
  2086. # The IEEE edition of the YT paper gives useful information on the
  2087. # optimal update order for the real poles in order to minimize the number
  2088. # of times we have to loop over all poles, see page 1442
  2089. nb_real = poles[np.isreal(poles)].shape[0]
  2090. # hnb => Half Nb Real
  2091. hnb = nb_real // 2
  2092. # Stick to the indices in the paper and then remove one to get numpy array
  2093. # index it is a bit easier to link the code to the paper this way even if it
  2094. # is not very clean. The paper is unclear about what should be done when
  2095. # there is only one real pole => use KNV0 on this real pole seem to work
  2096. if nb_real > 0:
  2097. #update the biggest real pole with the smallest one
  2098. update_order = [[nb_real], [1]]
  2099. else:
  2100. update_order = [[],[]]
  2101. r_comp = np.arange(nb_real+1, len(poles)+1, 2)
  2102. # step 1.a
  2103. r_p = np.arange(1, hnb+nb_real % 2)
  2104. update_order[0].extend(2*r_p)
  2105. update_order[1].extend(2*r_p+1)
  2106. # step 1.b
  2107. update_order[0].extend(r_comp)
  2108. update_order[1].extend(r_comp+1)
  2109. # step 1.c
  2110. r_p = np.arange(1, hnb+1)
  2111. update_order[0].extend(2*r_p-1)
  2112. update_order[1].extend(2*r_p)
  2113. # step 1.d
  2114. if hnb == 0 and np.isreal(poles[0]):
  2115. update_order[0].append(1)
  2116. update_order[1].append(1)
  2117. update_order[0].extend(r_comp)
  2118. update_order[1].extend(r_comp+1)
  2119. # step 2.a
  2120. r_j = np.arange(2, hnb+nb_real % 2)
  2121. for j in r_j:
  2122. for i in range(1, hnb+1):
  2123. update_order[0].append(i)
  2124. update_order[1].append(i+j)
  2125. # step 2.b
  2126. if hnb == 0 and np.isreal(poles[0]):
  2127. update_order[0].append(1)
  2128. update_order[1].append(1)
  2129. update_order[0].extend(r_comp)
  2130. update_order[1].extend(r_comp+1)
  2131. # step 2.c
  2132. r_j = np.arange(2, hnb+nb_real % 2)
  2133. for j in r_j:
  2134. for i in range(hnb+1, nb_real+1):
  2135. idx_1 = i+j
  2136. if idx_1 > nb_real:
  2137. idx_1 = i+j-nb_real
  2138. update_order[0].append(i)
  2139. update_order[1].append(idx_1)
  2140. # step 2.d
  2141. if hnb == 0 and np.isreal(poles[0]):
  2142. update_order[0].append(1)
  2143. update_order[1].append(1)
  2144. update_order[0].extend(r_comp)
  2145. update_order[1].extend(r_comp+1)
  2146. # step 3.a
  2147. for i in range(1, hnb+1):
  2148. update_order[0].append(i)
  2149. update_order[1].append(i+hnb)
  2150. # step 3.b
  2151. if hnb == 0 and np.isreal(poles[0]):
  2152. update_order[0].append(1)
  2153. update_order[1].append(1)
  2154. update_order[0].extend(r_comp)
  2155. update_order[1].extend(r_comp+1)
  2156. update_order = np.array(update_order).T-1
  2157. stop = False
  2158. nb_try = 0
  2159. while nb_try < maxiter and not stop:
  2160. det_transfer_matrixb = np.abs(np.linalg.det(transfer_matrix))
  2161. for i, j in update_order:
  2162. if i == j:
  2163. assert i == 0, "i!=0 for KNV call in YT"
  2164. assert np.isreal(poles[i]), "calling KNV on a complex pole"
  2165. _KNV0(B, ker_pole, transfer_matrix, i, poles)
  2166. else:
  2167. transfer_matrix_not_i_j = np.delete(transfer_matrix, (i, j),
  2168. axis=1)
  2169. # after merge of gh-4249 great speed improvements could be
  2170. # achieved using QR updates instead of full QR in the line below
  2171. #to debug with numpy qr uncomment the line below
  2172. #Q, _ = np.linalg.qr(transfer_matrix_not_i_j, mode="complete")
  2173. Q, _ = s_qr(transfer_matrix_not_i_j, mode="full")
  2174. if np.isreal(poles[i]):
  2175. assert np.isreal(poles[j]), "mixing real and complex " + \
  2176. "in YT_real" + str(poles)
  2177. _YT_real(ker_pole, Q, transfer_matrix, i, j)
  2178. else:
  2179. assert ~np.isreal(poles[i]), "mixing real and complex " + \
  2180. "in YT_real" + str(poles)
  2181. _YT_complex(ker_pole, Q, transfer_matrix, i, j)
  2182. det_transfer_matrix = np.max((np.sqrt(np.spacing(1)),
  2183. np.abs(np.linalg.det(transfer_matrix))))
  2184. cur_rtol = np.abs(
  2185. (det_transfer_matrix -
  2186. det_transfer_matrixb) /
  2187. det_transfer_matrix)
  2188. if cur_rtol < rtol and det_transfer_matrix > np.sqrt(np.spacing(1)):
  2189. # Convergence test from YT page 21
  2190. stop = True
  2191. nb_try += 1
  2192. return stop, cur_rtol, nb_try
  2193. def _KNV0_loop(ker_pole, transfer_matrix, poles, B, maxiter, rtol):
  2194. """
  2195. Loop over all poles one by one and apply KNV method 0 algorithm
  2196. """
  2197. # This method is useful only because we need to be able to call
  2198. # _KNV0 from YT without looping over all poles, otherwise it would
  2199. # have been fine to mix _KNV0_loop and _KNV0 in a single function
  2200. stop = False
  2201. nb_try = 0
  2202. while nb_try < maxiter and not stop:
  2203. det_transfer_matrixb = np.abs(np.linalg.det(transfer_matrix))
  2204. for j in range(B.shape[0]):
  2205. _KNV0(B, ker_pole, transfer_matrix, j, poles)
  2206. det_transfer_matrix = np.max((np.sqrt(np.spacing(1)),
  2207. np.abs(np.linalg.det(transfer_matrix))))
  2208. cur_rtol = np.abs((det_transfer_matrix - det_transfer_matrixb) /
  2209. det_transfer_matrix)
  2210. if cur_rtol < rtol and det_transfer_matrix > np.sqrt(np.spacing(1)):
  2211. # Convergence test from YT page 21
  2212. stop = True
  2213. nb_try += 1
  2214. return stop, cur_rtol, nb_try
  2215. def place_poles(A, B, poles, method="YT", rtol=1e-3, maxiter=30):
  2216. """
  2217. Compute K such that eigenvalues (A - dot(B, K))=poles.
  2218. K is the gain matrix such as the plant described by the linear system
  2219. ``AX+BU`` will have its closed-loop poles, i.e the eigenvalues ``A - B*K``,
  2220. as close as possible to those asked for in poles.
  2221. SISO, MISO and MIMO systems are supported.
  2222. Parameters
  2223. ----------
  2224. A, B : ndarray
  2225. State-space representation of linear system ``AX + BU``.
  2226. poles : array_like
  2227. Desired real poles and/or complex conjugates poles.
  2228. Complex poles are only supported with ``method="YT"`` (default).
  2229. method: {'YT', 'KNV0'}, optional
  2230. Which method to choose to find the gain matrix K. One of:
  2231. - 'YT': Yang Tits
  2232. - 'KNV0': Kautsky, Nichols, Van Dooren update method 0
  2233. See References and Notes for details on the algorithms.
  2234. rtol: float, optional
  2235. After each iteration the determinant of the eigenvectors of
  2236. ``A - B*K`` is compared to its previous value, when the relative
  2237. error between these two values becomes lower than `rtol` the algorithm
  2238. stops. Default is 1e-3.
  2239. maxiter: int, optional
  2240. Maximum number of iterations to compute the gain matrix.
  2241. Default is 30.
  2242. Returns
  2243. -------
  2244. full_state_feedback : Bunch object
  2245. full_state_feedback is composed of:
  2246. gain_matrix : 1-D ndarray
  2247. The closed loop matrix K such as the eigenvalues of ``A-BK``
  2248. are as close as possible to the requested poles.
  2249. computed_poles : 1-D ndarray
  2250. The poles corresponding to ``A-BK`` sorted as first the real
  2251. poles in increasing order, then the complex conjugates in
  2252. lexicographic order.
  2253. requested_poles : 1-D ndarray
  2254. The poles the algorithm was asked to place sorted as above,
  2255. they may differ from what was achieved.
  2256. X : 2-D ndarray
  2257. The transfer matrix such as ``X * diag(poles) = (A - B*K)*X``
  2258. (see Notes)
  2259. rtol : float
  2260. The relative tolerance achieved on ``det(X)`` (see Notes).
  2261. `rtol` will be NaN if it is possible to solve the system
  2262. ``diag(poles) = (A - B*K)``, or 0 when the optimization
  2263. algorithms can't do anything i.e when ``B.shape[1] == 1``.
  2264. nb_iter : int
  2265. The number of iterations performed before converging.
  2266. `nb_iter` will be NaN if it is possible to solve the system
  2267. ``diag(poles) = (A - B*K)``, or 0 when the optimization
  2268. algorithms can't do anything i.e when ``B.shape[1] == 1``.
  2269. Notes
  2270. -----
  2271. The Tits and Yang (YT), [2]_ paper is an update of the original Kautsky et
  2272. al. (KNV) paper [1]_. KNV relies on rank-1 updates to find the transfer
  2273. matrix X such that ``X * diag(poles) = (A - B*K)*X``, whereas YT uses
  2274. rank-2 updates. This yields on average more robust solutions (see [2]_
  2275. pp 21-22), furthermore the YT algorithm supports complex poles whereas KNV
  2276. does not in its original version. Only update method 0 proposed by KNV has
  2277. been implemented here, hence the name ``'KNV0'``.
  2278. KNV extended to complex poles is used in Matlab's ``place`` function, YT is
  2279. distributed under a non-free licence by Slicot under the name ``robpole``.
  2280. It is unclear and undocumented how KNV0 has been extended to complex poles
  2281. (Tits and Yang claim on page 14 of their paper that their method can not be
  2282. used to extend KNV to complex poles), therefore only YT supports them in
  2283. this implementation.
  2284. As the solution to the problem of pole placement is not unique for MIMO
  2285. systems, both methods start with a tentative transfer matrix which is
  2286. altered in various way to increase its determinant. Both methods have been
  2287. proven to converge to a stable solution, however depending on the way the
  2288. initial transfer matrix is chosen they will converge to different
  2289. solutions and therefore there is absolutely no guarantee that using
  2290. ``'KNV0'`` will yield results similar to Matlab's or any other
  2291. implementation of these algorithms.
  2292. Using the default method ``'YT'`` should be fine in most cases; ``'KNV0'``
  2293. is only provided because it is needed by ``'YT'`` in some specific cases.
  2294. Furthermore ``'YT'`` gives on average more robust results than ``'KNV0'``
  2295. when ``abs(det(X))`` is used as a robustness indicator.
  2296. [2]_ is available as a technical report on the following URL:
  2297. https://hdl.handle.net/1903/5598
  2298. References
  2299. ----------
  2300. .. [1] J. Kautsky, N.K. Nichols and P. van Dooren, "Robust pole assignment
  2301. in linear state feedback", International Journal of Control, Vol. 41
  2302. pp. 1129-1155, 1985.
  2303. .. [2] A.L. Tits and Y. Yang, "Globally convergent algorithms for robust
  2304. pole assignment by state feedback", IEEE Transactions on Automatic
  2305. Control, Vol. 41, pp. 1432-1452, 1996.
  2306. Examples
  2307. --------
  2308. A simple example demonstrating real pole placement using both KNV and YT
  2309. algorithms. This is example number 1 from section 4 of the reference KNV
  2310. publication ([1]_):
  2311. >>> import numpy as np
  2312. >>> from scipy import signal
  2313. >>> import matplotlib.pyplot as plt
  2314. >>> A = np.array([[ 1.380, -0.2077, 6.715, -5.676 ],
  2315. ... [-0.5814, -4.290, 0, 0.6750 ],
  2316. ... [ 1.067, 4.273, -6.654, 5.893 ],
  2317. ... [ 0.0480, 4.273, 1.343, -2.104 ]])
  2318. >>> B = np.array([[ 0, 5.679 ],
  2319. ... [ 1.136, 1.136 ],
  2320. ... [ 0, 0, ],
  2321. ... [-3.146, 0 ]])
  2322. >>> P = np.array([-0.2, -0.5, -5.0566, -8.6659])
  2323. Now compute K with KNV method 0, with the default YT method and with the YT
  2324. method while forcing 100 iterations of the algorithm and print some results
  2325. after each call.
  2326. >>> fsf1 = signal.place_poles(A, B, P, method='KNV0')
  2327. >>> fsf1.gain_matrix
  2328. array([[ 0.20071427, -0.96665799, 0.24066128, -0.10279785],
  2329. [ 0.50587268, 0.57779091, 0.51795763, -0.41991442]])
  2330. >>> fsf2 = signal.place_poles(A, B, P) # uses YT method
  2331. >>> fsf2.computed_poles
  2332. array([-8.6659, -5.0566, -0.5 , -0.2 ])
  2333. >>> fsf3 = signal.place_poles(A, B, P, rtol=-1, maxiter=100)
  2334. >>> fsf3.X
  2335. array([[ 0.52072442+0.j, -0.08409372+0.j, -0.56847937+0.j, 0.74823657+0.j],
  2336. [-0.04977751+0.j, -0.80872954+0.j, 0.13566234+0.j, -0.29322906+0.j],
  2337. [-0.82266932+0.j, -0.19168026+0.j, -0.56348322+0.j, -0.43815060+0.j],
  2338. [ 0.22267347+0.j, 0.54967577+0.j, -0.58387806+0.j, -0.40271926+0.j]])
  2339. The absolute value of the determinant of X is a good indicator to check the
  2340. robustness of the results, both ``'KNV0'`` and ``'YT'`` aim at maximizing
  2341. it. Below a comparison of the robustness of the results above:
  2342. >>> abs(np.linalg.det(fsf1.X)) < abs(np.linalg.det(fsf2.X))
  2343. True
  2344. >>> abs(np.linalg.det(fsf2.X)) < abs(np.linalg.det(fsf3.X))
  2345. True
  2346. Now a simple example for complex poles:
  2347. >>> A = np.array([[ 0, 7/3., 0, 0 ],
  2348. ... [ 0, 0, 0, 7/9. ],
  2349. ... [ 0, 0, 0, 0 ],
  2350. ... [ 0, 0, 0, 0 ]])
  2351. >>> B = np.array([[ 0, 0 ],
  2352. ... [ 0, 0 ],
  2353. ... [ 1, 0 ],
  2354. ... [ 0, 1 ]])
  2355. >>> P = np.array([-3, -1, -2-1j, -2+1j]) / 3.
  2356. >>> fsf = signal.place_poles(A, B, P, method='YT')
  2357. We can plot the desired and computed poles in the complex plane:
  2358. >>> t = np.linspace(0, 2*np.pi, 401)
  2359. >>> plt.plot(np.cos(t), np.sin(t), 'k--') # unit circle
  2360. >>> plt.plot(fsf.requested_poles.real, fsf.requested_poles.imag,
  2361. ... 'wo', label='Desired')
  2362. >>> plt.plot(fsf.computed_poles.real, fsf.computed_poles.imag, 'bx',
  2363. ... label='Placed')
  2364. >>> plt.grid()
  2365. >>> plt.axis('image')
  2366. >>> plt.axis([-1.1, 1.1, -1.1, 1.1])
  2367. >>> plt.legend(bbox_to_anchor=(1.05, 1), loc=2, numpoints=1)
  2368. """
  2369. # Move away all the inputs checking, it only adds noise to the code
  2370. update_loop, poles = _valid_inputs(A, B, poles, method, rtol, maxiter)
  2371. # The current value of the relative tolerance we achieved
  2372. cur_rtol = 0
  2373. # The number of iterations needed before converging
  2374. nb_iter = 0
  2375. # Step A: QR decomposition of B page 1132 KN
  2376. # to debug with numpy qr uncomment the line below
  2377. # u, z = np.linalg.qr(B, mode="complete")
  2378. u, z = s_qr(B, mode="full")
  2379. rankB = np.linalg.matrix_rank(B)
  2380. u0 = u[:, :rankB]
  2381. u1 = u[:, rankB:]
  2382. z = z[:rankB, :]
  2383. # If we can use the identity matrix as X the solution is obvious
  2384. if B.shape[0] == rankB:
  2385. # if B is square and full rank there is only one solution
  2386. # such as (A+BK)=inv(X)*diag(P)*X with X=eye(A.shape[0])
  2387. # i.e K=inv(B)*(diag(P)-A)
  2388. # if B has as many lines as its rank (but not square) there are many
  2389. # solutions and we can choose one using least squares
  2390. # => use lstsq in both cases.
  2391. # In both cases the transfer matrix X will be eye(A.shape[0]) and I
  2392. # can hardly think of a better one so there is nothing to optimize
  2393. #
  2394. # for complex poles we use the following trick
  2395. #
  2396. # |a -b| has for eigenvalues a+b and a-b
  2397. # |b a|
  2398. #
  2399. # |a+bi 0| has the obvious eigenvalues a+bi and a-bi
  2400. # |0 a-bi|
  2401. #
  2402. # e.g solving the first one in R gives the solution
  2403. # for the second one in C
  2404. diag_poles = np.zeros(A.shape)
  2405. idx = 0
  2406. while idx < poles.shape[0]:
  2407. p = poles[idx]
  2408. diag_poles[idx, idx] = np.real(p)
  2409. if ~np.isreal(p):
  2410. diag_poles[idx, idx+1] = -np.imag(p)
  2411. diag_poles[idx+1, idx+1] = np.real(p)
  2412. diag_poles[idx+1, idx] = np.imag(p)
  2413. idx += 1 # skip next one
  2414. idx += 1
  2415. gain_matrix = np.linalg.lstsq(B, diag_poles-A, rcond=-1)[0]
  2416. transfer_matrix = np.eye(A.shape[0])
  2417. cur_rtol = np.nan
  2418. nb_iter = np.nan
  2419. else:
  2420. # step A (p1144 KNV) and beginning of step F: decompose
  2421. # dot(U1.T, A-P[i]*I).T and build our set of transfer_matrix vectors
  2422. # in the same loop
  2423. ker_pole = []
  2424. # flag to skip the conjugate of a complex pole
  2425. skip_conjugate = False
  2426. # select orthonormal base ker_pole for each Pole and vectors for
  2427. # transfer_matrix
  2428. for j in range(B.shape[0]):
  2429. if skip_conjugate:
  2430. skip_conjugate = False
  2431. continue
  2432. pole_space_j = np.dot(u1.T, A-poles[j]*np.eye(B.shape[0])).T
  2433. # after QR Q=Q0|Q1
  2434. # only Q0 is used to reconstruct the qr'ed (dot Q, R) matrix.
  2435. # Q1 is orthogonal to Q0 and will be multiplied by the zeros in
  2436. # R when using mode "complete". In default mode Q1 and the zeros
  2437. # in R are not computed
  2438. # To debug with numpy qr uncomment the line below
  2439. # Q, _ = np.linalg.qr(pole_space_j, mode="complete")
  2440. Q, _ = s_qr(pole_space_j, mode="full")
  2441. ker_pole_j = Q[:, pole_space_j.shape[1]:]
  2442. # We want to select one vector in ker_pole_j to build the transfer
  2443. # matrix, however qr returns sometimes vectors with zeros on the
  2444. # same line for each pole and this yields very long convergence
  2445. # times.
  2446. # Or some other times a set of vectors, one with zero imaginary
  2447. # part and one (or several) with imaginary parts. After trying
  2448. # many ways to select the best possible one (eg ditch vectors
  2449. # with zero imaginary part for complex poles) I ended up summing
  2450. # all vectors in ker_pole_j, this solves 100% of the problems and
  2451. # is a valid choice for transfer_matrix.
  2452. # This way for complex poles we are sure to have a non zero
  2453. # imaginary part that way, and the problem of lines full of zeros
  2454. # in transfer_matrix is solved too as when a vector from
  2455. # ker_pole_j has a zero the other one(s) when
  2456. # ker_pole_j.shape[1]>1) for sure won't have a zero there.
  2457. transfer_matrix_j = np.sum(ker_pole_j, axis=1)[:, np.newaxis]
  2458. transfer_matrix_j = (transfer_matrix_j /
  2459. np.linalg.norm(transfer_matrix_j))
  2460. if ~np.isreal(poles[j]): # complex pole
  2461. transfer_matrix_j = np.hstack([np.real(transfer_matrix_j),
  2462. np.imag(transfer_matrix_j)])
  2463. ker_pole.extend([ker_pole_j, ker_pole_j])
  2464. # Skip next pole as it is the conjugate
  2465. skip_conjugate = True
  2466. else: # real pole, nothing to do
  2467. ker_pole.append(ker_pole_j)
  2468. if j == 0:
  2469. transfer_matrix = transfer_matrix_j
  2470. else:
  2471. transfer_matrix = np.hstack((transfer_matrix, transfer_matrix_j))
  2472. if rankB > 1: # otherwise there is nothing we can optimize
  2473. stop, cur_rtol, nb_iter = update_loop(ker_pole, transfer_matrix,
  2474. poles, B, maxiter, rtol)
  2475. if not stop and rtol > 0:
  2476. # if rtol<=0 the user has probably done that on purpose,
  2477. # don't annoy them
  2478. err_msg = (
  2479. "Convergence was not reached after maxiter iterations.\n"
  2480. f"You asked for a tolerance of {rtol}, we got {cur_rtol}."
  2481. )
  2482. warnings.warn(err_msg, stacklevel=2)
  2483. # reconstruct transfer_matrix to match complex conjugate pairs,
  2484. # ie transfer_matrix_j/transfer_matrix_j+1 are
  2485. # Re(Complex_pole), Im(Complex_pole) now and will be Re-Im/Re+Im after
  2486. transfer_matrix = transfer_matrix.astype(complex)
  2487. idx = 0
  2488. while idx < poles.shape[0]-1:
  2489. if ~np.isreal(poles[idx]):
  2490. rel = transfer_matrix[:, idx].copy()
  2491. img = transfer_matrix[:, idx+1]
  2492. # rel will be an array referencing a column of transfer_matrix
  2493. # if we don't copy() it will changer after the next line and
  2494. # and the line after will not yield the correct value
  2495. transfer_matrix[:, idx] = rel-1j*img
  2496. transfer_matrix[:, idx+1] = rel+1j*img
  2497. idx += 1 # skip next one
  2498. idx += 1
  2499. try:
  2500. m = np.linalg.solve(transfer_matrix.T, np.dot(np.diag(poles),
  2501. transfer_matrix.T)).T
  2502. gain_matrix = np.linalg.solve(z, np.dot(u0.T, m-A))
  2503. except np.linalg.LinAlgError as e:
  2504. raise ValueError("The poles you've chosen can't be placed. "
  2505. "Check the controllability matrix and try "
  2506. "another set of poles") from e
  2507. # Beware: Kautsky solves A+BK but the usual form is A-BK
  2508. gain_matrix = -gain_matrix
  2509. # K still contains complex with ~=0j imaginary parts, get rid of them
  2510. gain_matrix = np.real(gain_matrix)
  2511. full_state_feedback = Bunch()
  2512. full_state_feedback.gain_matrix = gain_matrix
  2513. full_state_feedback.computed_poles = _order_complex_poles(
  2514. np.linalg.eig(A - np.dot(B, gain_matrix))[0]
  2515. )
  2516. full_state_feedback.requested_poles = poles
  2517. full_state_feedback.X = transfer_matrix
  2518. full_state_feedback.rtol = cur_rtol
  2519. full_state_feedback.nb_iter = nb_iter
  2520. return full_state_feedback
  2521. def dlsim(system, u, t=None, x0=None):
  2522. r"""Simulate output of a discrete-time linear system.
  2523. Parameters
  2524. ----------
  2525. system : dlti | tuple
  2526. An instance of the LTI class `dlti` or a tuple describing the system.
  2527. The number of elements in the tuple determine the interpretation. I.e.:
  2528. * ``system``: Instance of LTI class `dlti`. Note that derived instances, such
  2529. as instances of `TransferFunction`, `ZerosPolesGain`, or `StateSpace`, are
  2530. allowed as well.
  2531. * ``(num, den, dt)``: Rational polynomial as described in `TransferFunction`.
  2532. The coefficients of the polynomials should be specified in descending
  2533. exponent order, e.g., z² + 3z + 5 would be represented as ``[1, 3, 5]``.
  2534. * ``(zeros, poles, gain, dt)``: Zeros, poles, gain form as described
  2535. in `ZerosPolesGain`.
  2536. * ``(A, B, C, D, dt)``: State-space form as described in `StateSpace`.
  2537. u : array_like
  2538. An input array describing the input at each time `t` (interpolation is
  2539. assumed between given times). If there are multiple inputs, then each
  2540. column of the rank-2 array represents an input.
  2541. t : array_like, optional
  2542. The time steps at which the input is defined. If `t` is given, it
  2543. must be the same length as `u`, and the final value in `t` determines
  2544. the number of steps returned in the output.
  2545. x0 : array_like, optional
  2546. The initial conditions on the state vector (zero by default).
  2547. Returns
  2548. -------
  2549. tout : ndarray
  2550. Time values for the output, as a 1-D array.
  2551. yout : ndarray
  2552. System response, as a 1-D array.
  2553. xout : ndarray, optional
  2554. Time-evolution of the state-vector. Only generated if the input is a
  2555. `StateSpace` system.
  2556. See Also
  2557. --------
  2558. lsim, dstep, dimpulse, cont2discrete
  2559. Examples
  2560. --------
  2561. A simple integrator transfer function with a discrete time step of 1.0
  2562. could be implemented as:
  2563. >>> import numpy as np
  2564. >>> from scipy import signal
  2565. >>> tf = ([1.0,], [1.0, -1.0], 1.0)
  2566. >>> t_in = [0.0, 1.0, 2.0, 3.0]
  2567. >>> u = np.asarray([0.0, 0.0, 1.0, 1.0])
  2568. >>> t_out, y = signal.dlsim(tf, u, t=t_in)
  2569. >>> y.T
  2570. array([[ 0., 0., 0., 1.]])
  2571. """
  2572. # Convert system to dlti-StateSpace
  2573. if isinstance(system, lti):
  2574. raise AttributeError('dlsim can only be used with discrete-time dlti '
  2575. 'systems.')
  2576. elif not isinstance(system, dlti):
  2577. system = dlti(*system[:-1], dt=system[-1])
  2578. # Condition needed to ensure output remains compatible
  2579. is_ss_input = isinstance(system, StateSpace)
  2580. system = system._as_ss()
  2581. u = np.atleast_1d(u)
  2582. if u.ndim == 1:
  2583. u = np.atleast_2d(u).T
  2584. if t is None:
  2585. out_samples = len(u)
  2586. stoptime = (out_samples - 1) * system.dt
  2587. else:
  2588. stoptime = t[-1]
  2589. out_samples = int(np.floor(stoptime / system.dt)) + 1
  2590. # Pre-build output arrays
  2591. xout = np.zeros((out_samples, system.A.shape[0]))
  2592. yout = np.zeros((out_samples, system.C.shape[0]))
  2593. tout = np.linspace(0.0, stoptime, num=out_samples)
  2594. # Check initial condition
  2595. if x0 is None:
  2596. xout[0, :] = np.zeros((system.A.shape[1],))
  2597. else:
  2598. xout[0, :] = np.asarray(x0)
  2599. # Pre-interpolate inputs into the desired time steps
  2600. if t is None:
  2601. u_dt = u
  2602. else:
  2603. if len(u.shape) == 1:
  2604. u = u[:, np.newaxis]
  2605. u_dt = make_interp_spline(t, u, k=1)(tout)
  2606. # Simulate the system
  2607. for i in range(0, out_samples - 1):
  2608. xout[i+1, :] = (np.dot(system.A, xout[i, :]) +
  2609. np.dot(system.B, u_dt[i, :]))
  2610. yout[i, :] = (np.dot(system.C, xout[i, :]) +
  2611. np.dot(system.D, u_dt[i, :]))
  2612. # Last point
  2613. yout[out_samples-1, :] = (np.dot(system.C, xout[out_samples-1, :]) +
  2614. np.dot(system.D, u_dt[out_samples-1, :]))
  2615. if is_ss_input:
  2616. return tout, yout, xout
  2617. else:
  2618. return tout, yout
  2619. def dimpulse(system, x0=None, t=None, n=None):
  2620. r"""Impulse response of discrete-time system.
  2621. Parameters
  2622. ----------
  2623. system : dlti | tuple
  2624. An instance of the LTI class `dlti` or a tuple describing the system.
  2625. The number of elements in the tuple determine the interpretation. I.e.:
  2626. * ``system``: Instance of LTI class `dlti`. Note that derived instances, such
  2627. as instances of `TransferFunction`, `ZerosPolesGain`, or `StateSpace`, are
  2628. allowed as well.
  2629. * ``(num, den, dt)``: Rational polynomial as described in `TransferFunction`.
  2630. The coefficients of the polynomials should be specified in descending
  2631. exponent order, e.g., z² + 3z + 5 would be represented as ``[1, 3, 5]``.
  2632. * ``(zeros, poles, gain, dt)``: Zeros, poles, gain form as described
  2633. in `ZerosPolesGain`.
  2634. * ``(A, B, C, D, dt)``: State-space form as described in `StateSpace`.
  2635. x0 : array_like, optional
  2636. Initial state-vector. Defaults to zero.
  2637. t : array_like, optional
  2638. Time points. Computed if not given.
  2639. n : int, optional
  2640. The number of time points to compute (if `t` is not given).
  2641. Returns
  2642. -------
  2643. tout : ndarray
  2644. Time values for the output, as a 1-D array.
  2645. yout : tuple of ndarray
  2646. Impulse response of system. Each element of the tuple represents
  2647. the output of the system based on an impulse in each input.
  2648. See Also
  2649. --------
  2650. impulse, dstep, dlsim, cont2discrete
  2651. Examples
  2652. --------
  2653. >>> import numpy as np
  2654. >>> from scipy import signal
  2655. >>> import matplotlib.pyplot as plt
  2656. ...
  2657. >>> dt = 1 # sampling interval is one => time unit is sample number
  2658. >>> bb, aa = signal.butter(3, 0.25, fs=1/dt)
  2659. >>> t, y = signal.dimpulse((bb, aa, dt), n=25)
  2660. ...
  2661. >>> fig0, ax0 = plt.subplots()
  2662. >>> ax0.step(t, np.squeeze(y), '.-', where='post')
  2663. >>> ax0.set_title(r"Impulse Response of a $3^\text{rd}$ Order Butterworth Filter")
  2664. >>> ax0.set(xlabel='Sample number', ylabel='Amplitude')
  2665. >>> ax0.grid()
  2666. >>> plt.show()
  2667. """
  2668. # Convert system to dlti-StateSpace
  2669. if isinstance(system, dlti):
  2670. system = system._as_ss()
  2671. elif isinstance(system, lti):
  2672. raise AttributeError('dimpulse can only be used with discrete-time '
  2673. 'dlti systems.')
  2674. else:
  2675. system = dlti(*system[:-1], dt=system[-1])._as_ss()
  2676. # Default to 100 samples if unspecified
  2677. if n is None:
  2678. n = 100
  2679. # If time is not specified, use the number of samples
  2680. # and system dt
  2681. if t is None:
  2682. t = np.linspace(0, n * system.dt, n, endpoint=False)
  2683. else:
  2684. t = np.asarray(t)
  2685. # For each input, implement a step change
  2686. yout = None
  2687. for i in range(0, system.inputs):
  2688. u = np.zeros((t.shape[0], system.inputs))
  2689. u[0, i] = 1.0
  2690. one_output = dlsim(system, u, t=t, x0=x0)
  2691. if yout is None:
  2692. yout = (one_output[1],)
  2693. else:
  2694. yout = yout + (one_output[1],)
  2695. tout = one_output[0]
  2696. return tout, yout
  2697. def dstep(system, x0=None, t=None, n=None):
  2698. r"""Step response of discrete-time system.
  2699. Parameters
  2700. ----------
  2701. system : dlti | tuple
  2702. An instance of the LTI class `dlti` or a tuple describing the system.
  2703. The number of elements in the tuple determine the interpretation. I.e.:
  2704. * ``system``: Instance of LTI class `dlti`. Note that derived instances, such
  2705. as instances of `TransferFunction`, `ZerosPolesGain`, or `StateSpace`, are
  2706. allowed as well.
  2707. * ``(num, den, dt)``: Rational polynomial as described in `TransferFunction`.
  2708. The coefficients of the polynomials should be specified in descending
  2709. exponent order, e.g., z² + 3z + 5 would be represented as ``[1, 3, 5]``.
  2710. * ``(zeros, poles, gain, dt)``: Zeros, poles, gain form as described
  2711. in `ZerosPolesGain`.
  2712. * ``(A, B, C, D, dt)``: State-space form as described in `StateSpace`.
  2713. x0 : array_like, optional
  2714. Initial state-vector. Defaults to zero.
  2715. t : array_like, optional
  2716. Time points. Computed if not given.
  2717. n : int, optional
  2718. The number of time points to compute (if `t` is not given).
  2719. Returns
  2720. -------
  2721. tout : ndarray
  2722. Output time points, as a 1-D array.
  2723. yout : tuple of ndarray
  2724. Step response of system. Each element of the tuple represents
  2725. the output of the system based on a step response to each input.
  2726. See Also
  2727. --------
  2728. step, dimpulse, dlsim, cont2discrete
  2729. Examples
  2730. --------
  2731. The following example illustrates how to create a digital Butterworth filer and
  2732. plot its step response:
  2733. >>> import numpy as np
  2734. >>> from scipy import signal
  2735. >>> import matplotlib.pyplot as plt
  2736. ...
  2737. >>> dt = 1 # sampling interval is one => time unit is sample number
  2738. >>> bb, aa = signal.butter(3, 0.25, fs=1/dt)
  2739. >>> t, y = signal.dstep((bb, aa, dt), n=25)
  2740. ...
  2741. >>> fig0, ax0 = plt.subplots()
  2742. >>> ax0.step(t, np.squeeze(y), '.-', where='post')
  2743. >>> ax0.set_title(r"Step Response of a $3^\text{rd}$ Order Butterworth Filter")
  2744. >>> ax0.set(xlabel='Sample number', ylabel='Amplitude', ylim=(0, 1.1*np.max(y)))
  2745. >>> ax0.grid()
  2746. >>> plt.show()
  2747. """
  2748. # Convert system to dlti-StateSpace
  2749. if isinstance(system, dlti):
  2750. system = system._as_ss()
  2751. elif isinstance(system, lti):
  2752. raise AttributeError('dstep can only be used with discrete-time dlti '
  2753. 'systems.')
  2754. else:
  2755. system = dlti(*system[:-1], dt=system[-1])._as_ss()
  2756. # Default to 100 samples if unspecified
  2757. if n is None:
  2758. n = 100
  2759. # If time is not specified, use the number of samples
  2760. # and system dt
  2761. if t is None:
  2762. t = np.linspace(0, n * system.dt, n, endpoint=False)
  2763. else:
  2764. t = np.asarray(t)
  2765. # For each input, implement a step change
  2766. yout = None
  2767. for i in range(0, system.inputs):
  2768. u = np.zeros((t.shape[0], system.inputs))
  2769. u[:, i] = np.ones((t.shape[0],))
  2770. one_output = dlsim(system, u, t=t, x0=x0)
  2771. if yout is None:
  2772. yout = (one_output[1],)
  2773. else:
  2774. yout = yout + (one_output[1],)
  2775. tout = one_output[0]
  2776. return tout, yout
  2777. def dfreqresp(system, w=None, n=10000, whole=False):
  2778. r"""
  2779. Calculate the frequency response of a discrete-time system.
  2780. Parameters
  2781. ----------
  2782. system : dlti | tuple
  2783. An instance of the LTI class `dlti` or a tuple describing the system.
  2784. The number of elements in the tuple determine the interpretation. I.e.:
  2785. * ``system``: Instance of LTI class `dlti`. Note that derived instances, such
  2786. as instances of `TransferFunction`, `ZerosPolesGain`, or `StateSpace`, are
  2787. allowed as well.
  2788. * ``(num, den, dt)``: Rational polynomial as described in `TransferFunction`.
  2789. The coefficients of the polynomials should be specified in descending
  2790. exponent order, e.g., z² + 3z + 5 would be represented as ``[1, 3, 5]``.
  2791. * ``(zeros, poles, gain, dt)``: Zeros, poles, gain form as described
  2792. in `ZerosPolesGain`.
  2793. * ``(A, B, C, D, dt)``: State-space form as described in `StateSpace`.
  2794. w : array_like, optional
  2795. Array of frequencies (in radians/sample). Magnitude and phase data is
  2796. calculated for every value in this array. If not given a reasonable
  2797. set will be calculated.
  2798. n : int, optional
  2799. Number of frequency points to compute if `w` is not given. The `n`
  2800. frequencies are logarithmically spaced in an interval chosen to
  2801. include the influence of the poles and zeros of the system.
  2802. whole : bool, optional
  2803. Normally, if 'w' is not given, frequencies are computed from 0 to the
  2804. Nyquist frequency, pi radians/sample (upper-half of unit-circle). If
  2805. `whole` is True, compute frequencies from 0 to 2*pi radians/sample.
  2806. Returns
  2807. -------
  2808. w : 1D ndarray
  2809. Frequency array [radians/sample]
  2810. H : 1D ndarray
  2811. Array of complex magnitude values
  2812. Notes
  2813. -----
  2814. If (num, den) is passed in for ``system``, coefficients for both the
  2815. numerator and denominator should be specified in descending exponent
  2816. order (e.g. ``z^2 + 3z + 5`` would be represented as ``[1, 3, 5]``).
  2817. .. versionadded:: 0.18.0
  2818. Examples
  2819. --------
  2820. The following example generates the Nyquist plot of the transfer function
  2821. :math:`H(z) = \frac{1}{z^2 + 2z + 3}` with a sampling time of 0.05 seconds:
  2822. >>> from scipy import signal
  2823. >>> import matplotlib.pyplot as plt
  2824. >>> sys = signal.TransferFunction([1], [1, 2, 3], dt=0.05) # construct H(z)
  2825. >>> w, H = signal.dfreqresp(sys)
  2826. ...
  2827. >>> fig0, ax0 = plt.subplots()
  2828. >>> ax0.plot(H.real, H.imag, label=r"$H(z=e^{+j\omega})$")
  2829. >>> ax0.plot(H.real, -H.imag, label=r"$H(z=e^{-j\omega})$")
  2830. >>> ax0.set_title(r"Nyquist Plot of $H(z) = 1 / (z^2 + 2z + 3)$")
  2831. >>> ax0.set(xlabel=r"$\text{Re}\{z\}$", ylabel=r"$\text{Im}\{z\}$",
  2832. ... xlim=(-0.2, 0.65), aspect='equal')
  2833. >>> ax0.plot(H[0].real, H[0].imag, 'k.') # mark H(exp(1j*w[0]))
  2834. >>> ax0.text(0.2, 0, r"$H(e^{j0})$")
  2835. >>> ax0.grid(True)
  2836. >>> ax0.legend()
  2837. >>> plt.show()
  2838. """
  2839. if not isinstance(system, dlti):
  2840. if isinstance(system, lti):
  2841. raise AttributeError('dfreqresp can only be used with '
  2842. 'discrete-time systems.')
  2843. system = dlti(*system[:-1], dt=system[-1])
  2844. if isinstance(system, StateSpace):
  2845. # No SS->ZPK code exists right now, just SS->TF->ZPK
  2846. system = system._as_tf()
  2847. if not isinstance(system, TransferFunction | ZerosPolesGain):
  2848. raise ValueError('Unknown system type')
  2849. if system.inputs != 1 or system.outputs != 1:
  2850. raise ValueError("dfreqresp requires a SISO (single input, single "
  2851. "output) system.")
  2852. if w is not None:
  2853. worN = w
  2854. else:
  2855. worN = n
  2856. if isinstance(system, TransferFunction):
  2857. # Convert numerator and denominator from polynomials in the variable
  2858. # 'z' to polynomials in the variable 'z^-1', as freqz expects.
  2859. num, den = TransferFunction._z_to_zinv(system.num.ravel(), system.den)
  2860. w, h = freqz(num, den, worN=worN, whole=whole)
  2861. elif isinstance(system, ZerosPolesGain):
  2862. w, h = freqz_zpk(system.zeros, system.poles, system.gain, worN=worN,
  2863. whole=whole)
  2864. return w, h
  2865. def dbode(system, w=None, n=100):
  2866. r"""Calculate Bode magnitude and phase data of a discrete-time system.
  2867. Parameters
  2868. ----------
  2869. system : dlti | tuple
  2870. An instance of the LTI class `dlti` or a tuple describing the system.
  2871. The number of elements in the tuple determine the interpretation. I.e.:
  2872. * ``system``: Instance of LTI class `dlti`. Note that derived instances, such
  2873. as instances of `TransferFunction`, `ZerosPolesGain`, or `StateSpace`, are
  2874. allowed as well.
  2875. * ``(num, den, dt)``: Rational polynomial as described in `TransferFunction`.
  2876. The coefficients of the polynomials should be specified in descending
  2877. exponent order, e.g., z² + 3z + 5 would be represented as ``[1, 3, 5]``.
  2878. * ``(zeros, poles, gain, dt)``: Zeros, poles, gain form as described
  2879. in `ZerosPolesGain`.
  2880. * ``(A, B, C, D, dt)``: State-space form as described in `StateSpace`.
  2881. w : array_like, optional
  2882. Array of frequencies normalized to the Nyquist frequency being π, i.e.,
  2883. having unit radiant / sample. Magnitude and phase data is calculated for every
  2884. value in this array. If not given, a reasonable set will be calculated.
  2885. n : int, optional
  2886. Number of frequency points to compute if `w` is not given. The `n`
  2887. frequencies are logarithmically spaced in an interval chosen to
  2888. include the influence of the poles and zeros of the system.
  2889. Returns
  2890. -------
  2891. w : 1D ndarray
  2892. Array of frequencies normalized to the Nyquist frequency being ``np.pi/dt``
  2893. with ``dt`` being the sampling interval of the `system` parameter.
  2894. The unit is rad/s assuming ``dt`` is in seconds.
  2895. mag : 1D ndarray
  2896. Magnitude array in dB
  2897. phase : 1D ndarray
  2898. Phase array in degrees
  2899. Notes
  2900. -----
  2901. This function is a convenience wrapper around `dfreqresp` for extracting
  2902. magnitude and phase from the calculated complex-valued amplitude of the
  2903. frequency response.
  2904. .. versionadded:: 0.18.0
  2905. See Also
  2906. --------
  2907. dfreqresp, dlti, TransferFunction, ZerosPolesGain, StateSpace
  2908. Examples
  2909. --------
  2910. The following example shows how to create a Bode plot of a 5-th order
  2911. Butterworth lowpass filter with a corner frequency of 100 Hz:
  2912. >>> import matplotlib.pyplot as plt
  2913. >>> import numpy as np
  2914. >>> from scipy import signal
  2915. ...
  2916. >>> T = 1e-4 # sampling interval in s
  2917. >>> f_c, o = 1e2, 5 # corner frequency in Hz (i.e., -3 dB value) and filter order
  2918. >>> bb, aa = signal.butter(o, f_c, 'lowpass', fs=1/T)
  2919. ...
  2920. >>> w, mag, phase = signal.dbode((bb, aa, T))
  2921. >>> w /= 2*np.pi # convert unit of frequency into Hertz
  2922. ...
  2923. >>> fg, (ax0, ax1) = plt.subplots(2, 1, sharex='all', figsize=(5, 4),
  2924. ... tight_layout=True)
  2925. >>> ax0.set_title("Bode Plot of Butterworth Lowpass Filter " +
  2926. ... rf"($f_c={f_c:g}\,$Hz, order={o})")
  2927. >>> ax0.set_ylabel(r"Magnitude in dB")
  2928. >>> ax1.set(ylabel=r"Phase in Degrees",
  2929. ... xlabel="Frequency $f$ in Hertz", xlim=(w[1], w[-1]))
  2930. >>> ax0.semilogx(w, mag, 'C0-', label=r"$20\,\log_{10}|G(f)|$") # Magnitude plot
  2931. >>> ax1.semilogx(w, phase, 'C1-', label=r"$\angle G(f)$") # Phase plot
  2932. ...
  2933. >>> for ax_ in (ax0, ax1):
  2934. ... ax_.axvline(f_c, color='m', alpha=0.25, label=rf"${f_c=:g}\,$Hz")
  2935. ... ax_.grid(which='both', axis='x') # plot major & minor vertical grid lines
  2936. ... ax_.grid(which='major', axis='y')
  2937. ... ax_.legend()
  2938. >>> plt.show()
  2939. """
  2940. w, y = dfreqresp(system, w=w, n=n)
  2941. if isinstance(system, dlti):
  2942. dt = system.dt
  2943. else:
  2944. dt = system[-1]
  2945. mag = 20.0 * np.log10(abs(y))
  2946. phase = np.rad2deg(np.unwrap(np.angle(y)))
  2947. return w / dt, mag, phase