From cd87f2f4ce7934720e1b25be59d76b247b154c05 Mon Sep 17 00:00:00 2001 From: Johannes Kaisinger Date: Thu, 4 Jul 2024 15:55:53 +0200 Subject: [PATCH 1/8] Implement ERA, change api to TimeResponseData --- control/modelsimp.py | 108 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 21 deletions(-) diff --git a/control/modelsimp.py b/control/modelsimp.py index 06c3d350d..11bc16240 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -368,38 +368,104 @@ 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 - `YY`. +def era(data, r, m=None, n=None, dt=True): + """Calculate an ERA model of order `r` based on the impulse-response data. - .. 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]_). 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 + data : TimeResponseData + impulse-response data 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 can be used to scale the StateSpace model in order to match + the impulse response of this library. + Default values is True. 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. + + 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) + >>> response = ct.impulse_response(ct.tf([1], [1, 0.5], True), T) + >>> sysd, _ = ct.era(response, r=1) """ - raise NotImplementedError('This function is not implemented yet.') + def block_hankel_matrix(Y, m, n): + + 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 + + Y = np.array(data.outputs, ndmin=3) + if data.transpose: + Y = np.transpose(Y) + q, p, l = Y.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(Y[:,:,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 + Cr = Hf[0:q,:] @ Vhr.T @ Sigma_inv + Dr = Y[:,:,0] + + return StateSpace(Ar,Br,Cr,Dr,dt), S def markov(Y, U, m=None, transpose=False): From 8a9c4974ddb5453b51569d16039816b5824d334c Mon Sep 17 00:00:00 2001 From: Johannes Kaisinger Date: Thu, 4 Jul 2024 19:01:31 +0200 Subject: [PATCH 2/8] Add era example --- examples/era_mkd.py | 64 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 examples/era_mkd.py diff --git a/examples/era_mkd.py b/examples/era_mkd.py new file mode 100644 index 000000000..e8486de40 --- /dev/null +++ b/examples/era_mkd.py @@ -0,0 +1,64 @@ +# mkd_era.py +# Johannes Kaisinger, 4 July 2024 +# +# Demonstrate estimation of markov parameters. +# 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) +# m q_dd + c q_d + k q = u +m1, k1, c1 = 1., 1., .1 +m2, k2, c2 = 2., .5, .1 +k3, c3 = .5, .1 + +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.5 +sysd = sys.sample(dt, method='zoh') +response = ct.impulse_response(sysd) +response.plot() +plt.show() + +sysd_est, _ = ct.era(response,r=4,dt=dt) + +step_true = ct.step_response(sysd) +step_est = ct.step_response(sysd_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() \ No newline at end of file From 562824c1c87e962ea34cdd6193aeaa87ace6fc51 Mon Sep 17 00:00:00 2001 From: Johannes Kaisinger Date: Thu, 4 Jul 2024 19:05:56 +0200 Subject: [PATCH 3/8] Rename era example file name, small clean up --- examples/{era_mkd.py => era_msd.py} | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) rename examples/{era_mkd.py => era_msd.py} (94%) diff --git a/examples/era_mkd.py b/examples/era_msd.py similarity index 94% rename from examples/era_mkd.py rename to examples/era_msd.py index e8486de40..f33a27a35 100644 --- a/examples/era_mkd.py +++ b/examples/era_msd.py @@ -1,18 +1,15 @@ -# mkd_era.py +# era_msd.py # Johannes Kaisinger, 4 July 2024 # -# Demonstrate estimation of markov parameters. +# 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) # m q_dd + c q_d + k q = u m1, k1, c1 = 1., 1., .1 @@ -58,7 +55,5 @@ plt.show() - if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: - plt.show() \ No newline at end of file From 6bbad5f1e61d31d81c36ca57834cdf2acc9f5d1d Mon Sep 17 00:00:00 2001 From: Johannes Kaisinger Date: Thu, 4 Jul 2024 19:18:36 +0200 Subject: [PATCH 4/8] Add era example to doc --- doc/era_msd.py | 1 + doc/era_msd.rst | 15 +++++++++++++++ doc/examples.rst | 1 + 3 files changed, 17 insertions(+) create mode 120000 doc/era_msd.py create mode 100644 doc/era_msd.rst diff --git a/doc/era_msd.py b/doc/era_msd.py new file mode 120000 index 000000000..0cf6a5282 --- /dev/null +++ b/doc/era_msd.py @@ -0,0 +1 @@ +../examples/era_msd.py \ No newline at end of file diff --git a/doc/era_msd.rst b/doc/era_msd.rst new file mode 100644 index 000000000..de702406e --- /dev/null +++ b/doc/era_msd.rst @@ -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 \ No newline at end of file diff --git a/doc/examples.rst b/doc/examples.rst index 21364157e..25e161159 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -35,6 +35,7 @@ other sources. kincar-flatsys mrac_siso_mit mrac_siso_lyapunov + era_msd Jupyter notebooks ================= From 614a0808a452c6658de58f869bb5eb1af383c35a Mon Sep 17 00:00:00 2001 From: Johannes Kaisinger Date: Sun, 7 Jul 2024 19:09:12 +0200 Subject: [PATCH 5/8] Change API to work with TimeResponseData and ndarray as input, add pytest, fix small things --- control/modelsimp.py | 47 +++++++++++++++---- control/tests/modelsimp_test.py | 83 ++++++++++++++++++++++++++++++++- examples/era_msd.py | 14 ++++-- 3 files changed, 128 insertions(+), 16 deletions(-) diff --git a/control/modelsimp.py b/control/modelsimp.py index 11bc16240..d3d934668 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -48,6 +48,7 @@ from .iosys import isdtime, isctime from .statesp import StateSpace from .statefbk import gram +from .timeresp import TimeResponseData __all__ = ['hsvd', 'balred', 'modred', 'era', 'markov', 'minreal'] @@ -368,8 +369,10 @@ def minreal(sys, tol=None, verbose=True): return sysr -def era(data, r, m=None, n=None, dt=True): - """Calculate an ERA model of order `r` based on the impulse-response data. +def era(arg, r, m=None, n=None, dt=True, transpose=False): + r"""era(YY, r) + + Calculate an ERA model of order `r` based on the impulse-response data. This function computes a discrete time system @@ -380,8 +383,19 @@ def era(data, r, m=None, n=None, dt=True): for a given impulse-response data (see [1]_). + The function can be called with 2 arguments: + + * ``sysd, S = era(data, r)`` + * ``sysd, S = era(YY, r)`` + + where `response` is an `TimeResponseData` object, and `YY` is 1D or 3D + array and r is an integer. + Parameters ---------- + YY : array_like + impulse-response data from which the StateSpace model is estimated, + 1D or 3D array. data : TimeResponseData impulse-response data from which the StateSpace model is estimated. r : integer @@ -398,11 +412,16 @@ def era(data, r, m=None, n=None, dt=True): It can be used to scale the StateSpace model in order to match the impulse response of this library. Default values is True. + transpose : bool, optional + Assume that input data is transposed relative to the standard + :ref:`time-series-convention`. For TimeResponseData this parameter + is ignored. + Default value is False. Returns ------- sys : StateSpace - A reduced order model sys=StateSpace(Ar,Br,Cr,Dr,dt) + 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. @@ -416,6 +435,10 @@ def era(data, r, m=None, n=None, dt=True): Examples -------- + >>> T = np.linspace(0, 10, 100) + >>> _, YY = ct.impulse_response(ct.tf([1], [1, 0.5], True), T) + >>> sysd, _ = ct.era(YY, r=1) + >>> T = np.linspace(0, 10, 100) >>> response = ct.impulse_response(ct.tf([1], [1, 0.5], True), T) >>> sysd, _ = ct.era(response, r=1) @@ -434,10 +457,16 @@ def block_hankel_matrix(Y, m, n): return H - Y = np.array(data.outputs, ndmin=3) - if data.transpose: - Y = np.transpose(Y) - q, p, l = Y.shape + 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 @@ -450,7 +479,7 @@ def block_hankel_matrix(Y, m, n): if (l-1) < m+n: raise ValueError("Not enough data for requested number of parameters") - H = block_hankel_matrix(Y[:,:,1:], m, n+1) # Hankel matrix (q*m, p*(n+1)) + 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 @@ -463,7 +492,7 @@ def block_hankel_matrix(Y, m, n): Ar = Sigma_inv @ Ur.T @ Hl @ Vhr.T @ Sigma_inv Br = Sigma_inv @ Ur.T @ Hf[:,0:p]*dt Cr = Hf[0:q,:] @ Vhr.T @ Sigma_inv - Dr = Y[:,:,0] + Dr = YY[:,:,0] return StateSpace(Ar,Br,Cr,Dr,dt), S diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py index 49c2afd58..39277fb4f 100644 --- a/control/tests/modelsimp_test.py +++ b/control/tests/modelsimp_test.py @@ -7,10 +7,10 @@ import pytest -from control import StateSpace, forced_response, tf, rss, c2d +from control import StateSpace, impulse_response, step_response, forced_response, tf, rss, c2d from control.exception import ControlMIMONotImplemented from control.tests.conftest import slycotonly -from control.modelsimp import balred, hsvd, markov, modred +from control.modelsimp import balred, hsvd, markov, modred, era class TestModelsimp: @@ -111,6 +111,85 @@ def testMarkovResults(self, k, m, n): # for k=5, m=n=10: 0.015 % np.testing.assert_allclose(Mtrue, Mcomp, rtol=1e-6, atol=1e-8) + def testERASignature(self): + + # test siso + # Katayama, Subspace Methods for System Identification + # Example 6.1, Fibonacci sequence + H_true = np.array([0.,1.,1.,2.,3.,5.,8.,13.,21.,34.]) + + # A realization of fibonacci impulse response + A = np.array([[0., 1.],[1., 1.,]]) + B = np.array([[1.],[1.,]]) + C = np.array([[1., 0.,]]) + D = np.array([[0.,]]) + + T = np.arange(0,10,1) + sysd_true = StateSpace(A,B,C,D,True) + ir_true = impulse_response(sysd_true,T=T) + + # test TimeResponseData + sysd_est, _ = era(ir_true,r=2) + ir_est = impulse_response(sysd_est, T=T) + _, H_est = ir_est + + np.testing.assert_allclose(H_true, H_est, rtol=1e-6, atol=1e-8) + + # test ndarray + _, YY_true = ir_true + sysd_est, _ = era(YY_true,r=2) + ir_est = impulse_response(sysd_est, T=T) + _, H_est = ir_est + + np.testing.assert_allclose(H_true, H_est, rtol=1e-6, atol=1e-8) + + # test mimo + # 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)) + + sys = StateSpace(A, B, C, D) + + dt = 0.1 + T = np.arange(0,10,dt) + sysd_true = sys.sample(dt, method='zoh') + ir_true = impulse_response(sysd_true, T=T) + + # test TimeResponseData + sysd_est, _ = era(ir_true,r=4,dt=dt) + + step_true = step_response(sysd_true) + step_est = step_response(sysd_est) + + np.testing.assert_allclose(step_true.outputs, + step_est.outputs, + rtol=1e-6, atol=1e-8) + + # test ndarray + _, YY_true = ir_true + sysd_est, _ = era(YY_true,r=4,dt=dt) + + step_true = step_response(sysd_true, T=T) + step_est = step_response(sysd_est, T=T) + + np.testing.assert_allclose(step_true.outputs, + step_est.outputs, + rtol=1e-6, atol=1e-8) + + def testModredMatchDC(self): #balanced realization computed in matlab for the transfer function: # num = [1 11 45 32], den = [1 15 60 200 60] diff --git a/examples/era_msd.py b/examples/era_msd.py index f33a27a35..9a5fc8c4d 100644 --- a/examples/era_msd.py +++ b/examples/era_msd.py @@ -11,10 +11,12 @@ import control as ct # set up a mass spring damper system (2dof, MIMO case) -# m q_dd + c q_d + k q = u -m1, k1, c1 = 1., 1., .1 -m2, k2, c2 = 2., .5, .1 -k3, c3 = .5, .1 +# 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.], @@ -39,7 +41,7 @@ sys = ct.StateSpace(A, B, C, D) -dt = 0.5 +dt = 0.1 sysd = sys.sample(dt, method='zoh') response = ct.impulse_response(sysd) response.plot() @@ -48,7 +50,9 @@ sysd_est, _ = ct.era(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') From 250c448a3e31fac9fb035cf01f938f4f2e9225e1 Mon Sep 17 00:00:00 2001 From: Johannes Kaisinger Date: Thu, 11 Jul 2024 16:04:16 +0200 Subject: [PATCH 6/8] Fix few docstring things, change name to eigensys_realization --- control/modelsimp.py | 57 ++++++++++++++++----------------- control/tests/modelsimp_test.py | 10 +++--- doc/control.rst | 2 +- examples/era_msd.py | 2 +- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/control/modelsimp.py b/control/modelsimp.py index d3d934668..ca2a05e5c 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -50,7 +50,7 @@ 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 @@ -369,10 +369,11 @@ def minreal(sys, tol=None, verbose=True): return sysr -def era(arg, r, m=None, n=None, dt=True, transpose=False): - r"""era(YY, r) +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. + Calculate an ERA model of order `r` based on the impulse-response data + `YY`. This function computes a discrete time system @@ -385,8 +386,8 @@ def era(arg, r, m=None, n=None, dt=True, transpose=False): The function can be called with 2 arguments: - * ``sysd, S = era(data, r)`` - * ``sysd, S = era(YY, r)`` + * ``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. @@ -394,64 +395,59 @@ def era(arg, r, m=None, n=None, dt=True, transpose=False): Parameters ---------- YY : array_like - impulse-response data from which the StateSpace model is estimated, - 1D or 3D array. + Impulse-response from which the StateSpace model is estimated, 1D + or 3D array. data : TimeResponseData - impulse-response data from which the StateSpace model is estimated. + 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. + Number of rows in Hankel matrix. Default is 2*r. n : integer, optional - Number of columns in Hankel matrix. - Default is 2*r. + 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 can be used to scale the StateSpace model in order to match - the impulse response of this library. - Default values is True. + positive number is discrete time with specified sampling time. It + can be used to scale the StateSpace model in order to match the + impulse response of this library. Default is True. transpose : bool, optional Assume that input data is transposed relative to the standard :ref:`time-series-convention`. For TimeResponseData this parameter - is ignored. - Default value is False. + is ignored. Default is False. Returns ------- 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. + Singular values of Hankel matrix. Can be used to choose a good r + value. References ---------- - .. [1] Samet Oymak and Necmiye Ozay - Non-asymptotic Identification of LTI Systems - from a Single Trajectory. + .. [1] Samet Oymak and Necmiye Ozay, Non-asymptotic Identification of + LTI Systems from a Single Trajectory. https://arxiv.org/abs/1806.05722 Examples -------- >>> T = np.linspace(0, 10, 100) >>> _, YY = ct.impulse_response(ct.tf([1], [1, 0.5], True), T) - >>> sysd, _ = ct.era(YY, r=1) + >>> 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.era(response, r=1) + >>> sysd, _ = ct.eigensys_realization(response, r=1) """ def block_hankel_matrix(Y, m, n): - + """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 + # 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)) @@ -477,7 +473,7 @@ def block_hankel_matrix(Y, m, n): raise ValueError("Hankel parameters are to small") if (l-1) < m+n: - raise ValueError("Not enough data for requested number of parameters") + 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 @@ -651,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 diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py index 39277fb4f..dc50ce963 100644 --- a/control/tests/modelsimp_test.py +++ b/control/tests/modelsimp_test.py @@ -10,7 +10,7 @@ from control import StateSpace, impulse_response, step_response, forced_response, tf, rss, c2d from control.exception import ControlMIMONotImplemented from control.tests.conftest import slycotonly -from control.modelsimp import balred, hsvd, markov, modred, era +from control.modelsimp import balred, hsvd, markov, modred, eigensys_realization class TestModelsimp: @@ -129,7 +129,7 @@ def testERASignature(self): ir_true = impulse_response(sysd_true,T=T) # test TimeResponseData - sysd_est, _ = era(ir_true,r=2) + sysd_est, _ = eigensys_realization(ir_true,r=2) ir_est = impulse_response(sysd_est, T=T) _, H_est = ir_est @@ -137,7 +137,7 @@ def testERASignature(self): # test ndarray _, YY_true = ir_true - sysd_est, _ = era(YY_true,r=2) + sysd_est, _ = eigensys_realization(YY_true,r=2) ir_est = impulse_response(sysd_est, T=T) _, H_est = ir_est @@ -169,7 +169,7 @@ def testERASignature(self): ir_true = impulse_response(sysd_true, T=T) # test TimeResponseData - sysd_est, _ = era(ir_true,r=4,dt=dt) + sysd_est, _ = eigensys_realization(ir_true,r=4,dt=dt) step_true = step_response(sysd_true) step_est = step_response(sysd_est) @@ -180,7 +180,7 @@ def testERASignature(self): # test ndarray _, YY_true = ir_true - sysd_est, _ = era(YY_true,r=4,dt=dt) + sysd_est, _ = eigensys_realization(YY_true,r=4,dt=dt) step_true = step_response(sysd_true, T=T) step_est = step_response(sysd_est, T=T) diff --git a/doc/control.rst b/doc/control.rst index efd643d8a..30ae1a03b 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -137,7 +137,7 @@ Model simplification tools balred hsvd modred - era + eigensys_realization markov Nonlinear system support diff --git a/examples/era_msd.py b/examples/era_msd.py index 9a5fc8c4d..101933435 100644 --- a/examples/era_msd.py +++ b/examples/era_msd.py @@ -47,7 +47,7 @@ response.plot() plt.show() -sysd_est, _ = ct.era(response,r=4,dt=dt) +sysd_est, _ = ct.eigensys_realization(response,r=4,dt=dt) step_true = ct.step_response(sysd) step_true.sysname="H_true" From 6c7306217070252898aa9fa78d1ef06e37d5bfda Mon Sep 17 00:00:00 2001 From: Johannes Kaisinger Date: Mon, 15 Jul 2024 09:26:42 +0200 Subject: [PATCH 7/8] Change _block_hankel to top-level, update docstrings and comments --- control/modelsimp.py | 45 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/control/modelsimp.py b/control/modelsimp.py index ca2a05e5c..9fa3e4127 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -369,6 +369,21 @@ def minreal(sys, tol=None, verbose=True): return sysr +def _block_hankel(Y, m, n): + """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 + + def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False): r"""eigensys_realization(YY, r) @@ -389,8 +404,8 @@ def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False): * ``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. + where `data` is a `TimeResponseData` object, `YY` is a 1D or 3D + array, and r is an integer. Parameters ---------- @@ -406,10 +421,10 @@ def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False): 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 - can be used to scale the StateSpace model in order to match the - impulse response of this library. Default is True. + True indicates discrete time with unspecified sampling time and a + positive float is discrete time with the specified sampling time. + It can be used to scale the StateSpace model in order to match the + unit-area impulse response of python-control. Default is True. transpose : bool, optional Assume that input data is transposed relative to the standard :ref:`time-series-convention`. For TimeResponseData this parameter @@ -439,20 +454,6 @@ def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False): >>> response = ct.impulse_response(ct.tf([1], [1, 0.5], True), T) >>> sysd, _ = ct.eigensys_realization(response, r=1) """ - def block_hankel_matrix(Y, m, n): - """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: @@ -475,7 +476,7 @@ def block_hankel_matrix(Y, m, n): 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)) + H = _block_hankel(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 @@ -486,7 +487,7 @@ def block_hankel_matrix(Y, m, n): # 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 + Br = Sigma_inv @ Ur.T @ Hf[:,0:p]*dt # dt scaling for unit-area impulse Cr = Hf[0:q,:] @ Vhr.T @ Sigma_inv Dr = YY[:,:,0] From 8b9326435142eec4d07a6ec35bf0884fd3d13843 Mon Sep 17 00:00:00 2001 From: Johannes Kaisinger Date: Mon, 15 Jul 2024 09:55:04 +0200 Subject: [PATCH 8/8] Delete the hyphen from the Impuse response. --- control/modelsimp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/modelsimp.py b/control/modelsimp.py index 9fa3e4127..4b72e2bb9 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -410,10 +410,10 @@ def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False): Parameters ---------- YY : array_like - Impulse-response from which the StateSpace model is estimated, 1D + Impulse response from which the StateSpace model is estimated, 1D or 3D array. data : TimeResponseData - Impulse-response from which the StateSpace model is estimated. + Impulse response from which the StateSpace model is estimated. r : integer Order of model. m : integer, optional