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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy