Repository navigation
Implement ERA, change api to TimeResponseData #1024
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
cd87f2f
8a9c497
562824c
6bbad5f
614a080
250c448
6c73062
8b93264
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -48,8 +48,9 @@ | |||||
| from .iosys import isdtime, isctime | ||||||
| from .statesp import StateSpace | ||||||
| from .statefbk import gram | ||||||
| from .timeresp import TimeResponseData | ||||||
|
|
||||||
| __all__ = ['hsvd', 'balred', 'modred', 'era', 'markov', 'minreal'] | ||||||
| __all__ = ['hsvd', 'balred', 'modred', 'eigensys_realization', 'markov', 'minreal', 'era'] | ||||||
|
|
||||||
|
|
||||||
| # Hankel Singular Value Decomposition | ||||||
|
|
@@ -368,38 +369,128 @@ def minreal(sys, tol=None, verbose=True): | |||||
| return sysr | ||||||
|
|
||||||
|
|
||||||
| def era(YY, m, n, nin, nout, r): | ||||||
| """Calculate an ERA model of order `r` based on the impulse-response data | ||||||
| def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False): | ||||||
| r"""eigensys_realization(YY, r) | ||||||
|
|
||||||
| Calculate an ERA model of order `r` based on the impulse-response data | ||||||
| `YY`. | ||||||
|
|
||||||
| .. note:: This function is not implemented yet. | ||||||
| This function computes a discrete time system | ||||||
|
|
||||||
| .. math:: | ||||||
|
|
||||||
| x[k+1] &= A x[k] + B u[k] \\\\ | ||||||
| y[k] &= C x[k] + D u[k] | ||||||
|
|
||||||
| for a given impulse-response data (see [1]_). | ||||||
|
|
||||||
| The function can be called with 2 arguments: | ||||||
|
|
||||||
| * ``sysd, S = eigensys_realization(data, r)`` | ||||||
| * ``sysd, S = eigensys_realization(YY, r)`` | ||||||
|
|
||||||
| where `response` is an `TimeResponseData` object, and `YY` is 1D or 3D | ||||||
| array and r is an integer. | ||||||
|
|
||||||
| Parameters | ||||||
| ---------- | ||||||
| YY: array | ||||||
| `nout` x `nin` dimensional impulse-response data | ||||||
| m: integer | ||||||
| Number of rows in Hankel matrix | ||||||
| n: integer | ||||||
| Number of columns in Hankel matrix | ||||||
| nin: integer | ||||||
| Number of input variables | ||||||
| nout: integer | ||||||
| Number of output variables | ||||||
| r: integer | ||||||
| Order of model | ||||||
| YY : array_like | ||||||
| Impulse-response from which the StateSpace model is estimated, 1D | ||||||
| or 3D array. | ||||||
| data : TimeResponseData | ||||||
| Impulse-response from which the StateSpace model is estimated. | ||||||
| r : integer | ||||||
| Order of model. | ||||||
| m : integer, optional | ||||||
| Number of rows in Hankel matrix. Default is 2*r. | ||||||
| n : integer, optional | ||||||
| Number of columns in Hankel matrix. Default is 2*r. | ||||||
| dt : True or float, optional | ||||||
| True indicates discrete time with unspecified sampling time, | ||||||
| positive number is discrete time with specified sampling time. It | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||
| can be used to scale the StateSpace model in order to match the | ||||||
| impulse response of this library. Default is True. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand the idea of "impulse response of this library" - what does library mean here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This library "python-control" uses a unit area impulse "delta = 1/dt" for dt > 0 and a unit impulse "delta = 1" for dt = True. The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done! I change the text to " It can be used to scale the StateSpace model in order to match the unit-area impulse response of python-control. " Is there a better way to explain that behavior. Should we add something to Library conventions in order to explain the unit-area impulse response of python-control?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, thanks, now I understand. One possibly-not-obvious side-effect of using The unit-area convention is documented in |
||||||
| transpose : bool, optional | ||||||
| Assume that input data is transposed relative to the standard | ||||||
| :ref:`time-series-convention`. For TimeResponseData this parameter | ||||||
| is ignored. Default is False. | ||||||
|
|
||||||
| Returns | ||||||
| ------- | ||||||
| sys: StateSpace | ||||||
| A reduced order model sys=ss(Ar,Br,Cr,Dr) | ||||||
| sys : StateSpace | ||||||
| A reduced order model sys=StateSpace(Ar,Br,Cr,Dr,dt). | ||||||
| S : array | ||||||
| Singular values of Hankel matrix. Can be used to choose a good r | ||||||
| value. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. given this, is there an alternative interface with a This probably a bit out-of-scope, and if the user needs this capability it is simple enough to implement in their own code.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have thought about it, but I will not be adding this feature at this time. I don't know the literature on this topic well enough, but I know that there are papers that study the choice of r.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Postponed. Possible improvement in the future. |
||||||
|
|
||||||
| References | ||||||
| ---------- | ||||||
| .. [1] Samet Oymak and Necmiye Ozay, Non-asymptotic Identification of | ||||||
| LTI Systems from a Single Trajectory. | ||||||
| https://arxiv.org/abs/1806.05722 | ||||||
|
|
||||||
| Examples | ||||||
| -------- | ||||||
| >>> rsys = era(YY, m, n, nin, nout, r) # doctest: +SKIP | ||||||
| >>> T = np.linspace(0, 10, 100) | ||||||
| >>> _, YY = ct.impulse_response(ct.tf([1], [1, 0.5], True), T) | ||||||
| >>> sysd, _ = ct.eigensys_realization(YY, r=1) | ||||||
|
|
||||||
| >>> T = np.linspace(0, 10, 100) | ||||||
| >>> response = ct.impulse_response(ct.tf([1], [1, 0.5], True), T) | ||||||
| >>> sysd, _ = ct.eigensys_realization(response, r=1) | ||||||
| """ | ||||||
| raise NotImplementedError('This function is not implemented yet.') | ||||||
| def block_hankel_matrix(Y, m, n): | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggest making this top-level
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done! |
||||||
| """Create a block Hankel matrix from Impulse response""" | ||||||
| q, p, _ = Y.shape | ||||||
| YY = Y.transpose(0,2,1) # transpose for reshape | ||||||
|
|
||||||
| H = np.zeros((q*m,p*n)) | ||||||
|
|
||||||
| for r in range(m): | ||||||
| # shift and add row to Hankel matrix | ||||||
| new_row = YY[:,r:r+n,:] | ||||||
| H[q*r:q*(r+1),:] = new_row.reshape((q,p*n)) | ||||||
|
|
||||||
| return H | ||||||
|
|
||||||
| if isinstance(arg, TimeResponseData): | ||||||
| YY = np.array(arg.outputs, ndmin=3) | ||||||
| if arg.transpose: | ||||||
| YY = np.transpose(YY) | ||||||
| else: | ||||||
| YY = np.array(arg, ndmin=3) | ||||||
| if transpose: | ||||||
| YY = np.transpose(YY) | ||||||
|
|
||||||
| q, p, l = YY.shape | ||||||
|
|
||||||
| if m is None: | ||||||
| m = 2*r | ||||||
| if n is None: | ||||||
| n = 2*r | ||||||
|
|
||||||
| if m*q < r or n*p < r: | ||||||
| raise ValueError("Hankel parameters are to small") | ||||||
|
|
||||||
| if (l-1) < m+n: | ||||||
| raise ValueError("not enough data for requested number of parameters") | ||||||
|
|
||||||
| H = block_hankel_matrix(YY[:,:,1:], m, n+1) # Hankel matrix (q*m, p*(n+1)) | ||||||
| Hf = H[:,:-p] # first p*n columns of H | ||||||
| Hl = H[:,p:] # last p*n columns of H | ||||||
|
|
||||||
| U,S,Vh = np.linalg.svd(Hf, True) | ||||||
| Ur =U[:,0:r] | ||||||
| Vhr =Vh[0:r,:] | ||||||
|
|
||||||
| # balanced realizations | ||||||
| Sigma_inv = np.diag(1./np.sqrt(S[0:r])) | ||||||
| Ar = Sigma_inv @ Ur.T @ Hl @ Vhr.T @ Sigma_inv | ||||||
| Br = Sigma_inv @ Ur.T @ Hf[:,0:p]*dt | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like the reference here is "Algorithm 1" of the arxiv paper. Why does
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ERA in the paper assumes a unit impulse response "delta = 1". There is no dt in the paper. However, python control generates a unit-area impulse response "delta = 1/dt". See also: #812, #977. In other words, the impulse responses of StateSpace(A,B,C,D,dt=True) and StateSpace(A,B,C,D,dt=0.1) differ by a scaling. Alternative: Overall, my goal is that impulse_response, era, markov and StateSpace should work well together.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. I added a little comment to indicate the intentional use of Br*dt. Is there a more elegant way to achieve this? |
||||||
| Cr = Hf[0:q,:] @ Vhr.T @ Sigma_inv | ||||||
| Dr = YY[:,:,0] | ||||||
|
|
||||||
| return StateSpace(Ar,Br,Cr,Dr,dt), S | ||||||
|
|
||||||
|
|
||||||
| def markov(Y, U, m=None, transpose=False): | ||||||
|
|
@@ -556,3 +647,6 @@ def markov(Y, U, m=None, transpose=False): | |||||
|
|
||||||
| # Return the first m Markov parameters | ||||||
| return H if transpose else np.transpose(H) | ||||||
|
|
||||||
| # Function aliases | ||||||
| era = eigensys_realization | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../examples/era_msd.py |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| ERA example, mass spring damper system | ||
| -------------------------------------- | ||
|
|
||
| Code | ||
| .... | ||
| .. literalinclude:: era_msd.py | ||
| :language: python | ||
| :linenos: | ||
|
|
||
|
|
||
| Notes | ||
| ..... | ||
|
|
||
| 1. The environment variable `PYCONTROL_TEST_EXAMPLES` is used for | ||
| testing to turn off plotting of the outputs.0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # era_msd.py | ||
| # Johannes Kaisinger, 4 July 2024 | ||
| # | ||
| # Demonstrate estimation of State Space model from impulse response. | ||
| # SISO, SIMO, MISO, MIMO case | ||
|
|
||
| import numpy as np | ||
| import matplotlib.pyplot as plt | ||
| import os | ||
|
|
||
| import control as ct | ||
|
|
||
| # set up a mass spring damper system (2dof, MIMO case) | ||
| # Mechanical Vibrations: Theory and Application, SI Edition, 1st ed. | ||
| # Figure 6.5 / Example 6.7 | ||
| # m q_dd + c q_d + k q = f | ||
| m1, k1, c1 = 1., 4., 1. | ||
| m2, k2, c2 = 2., 2., 1. | ||
| k3, c3 = 6., 2. | ||
|
|
||
| A = np.array([ | ||
| [0., 0., 1., 0.], | ||
| [0., 0., 0., 1.], | ||
| [-(k1+k2)/m1, (k2)/m1, -(c1+c2)/m1, c2/m1], | ||
| [(k2)/m2, -(k2+k3)/m2, c2/m2, -(c2+c3)/m2] | ||
| ]) | ||
| B = np.array([[0.,0.],[0.,0.],[1/m1,0.],[0.,1/m2]]) | ||
| C = np.array([[1.0, 0.0, 0.0, 0.0],[0.0, 1.0, 0.0, 0.0]]) | ||
| D = np.zeros((2,2)) | ||
|
|
||
| xixo_list = ["SISO","SIMO","MISO","MIMO"] | ||
| xixo = xixo_list[3] # choose a system for estimation | ||
| match xixo: | ||
| case "SISO": | ||
| sys = ct.StateSpace(A, B[:,0], C[0,:], D[0,0]) | ||
| case "SIMO": | ||
| sys = ct.StateSpace(A, B[:,:1], C, D[:,:1]) | ||
| case "MISO": | ||
| sys = ct.StateSpace(A, B, C[:1,:], D[:1,:]) | ||
| case "MIMO": | ||
| sys = ct.StateSpace(A, B, C, D) | ||
|
|
||
|
|
||
| dt = 0.1 | ||
| sysd = sys.sample(dt, method='zoh') | ||
| response = ct.impulse_response(sysd) | ||
| response.plot() | ||
| plt.show() | ||
|
|
||
| sysd_est, _ = ct.eigensys_realization(response,r=4,dt=dt) | ||
|
|
||
| step_true = ct.step_response(sysd) | ||
| step_true.sysname="H_true" | ||
| step_est = ct.step_response(sysd_est) | ||
| step_est.sysname="H_est" | ||
|
|
||
| step_true.plot(title=xixo) | ||
| step_est.plot(color='orange',linestyle='dashed') | ||
|
|
||
| plt.show() | ||
|
|
||
| if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: | ||
| plt.show() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
response is more specific than data, perhaps better?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
responseordata? Not sure.For me, the function time_response_plot which uses
datafor time_response, is the blueprint.The plotting functions use
datafor input.bode_plot
The response data types use
responsefor input.NyquistResponseData
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, in my PR #1022 I initially used
responseinstead ofdata. I will go withdatafor now.@roryyorke @murrayrm what should we use?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer
responseas it's more specific, but the main thing is to be consistent within the ERA docstring(s).