Skip to content

Commit 5120bcc

Browse files
committed
Merge branch 'KybernetikJo-implement_era'
2 parents 9809a78 + 4c7977b commit 5120bcc

7 files changed

Lines changed: 278 additions & 24 deletions

File tree

control/modelsimp.py

Lines changed: 114 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
from .statefbk import gram
5050
from .timeresp import TimeResponseData
5151

52-
__all__ = ['hsvd', 'balred', 'modred', 'era', 'markov', 'minreal']
52+
__all__ = ['hsvd', 'balred', 'modred', 'eigensys_realization', 'markov', 'minreal', 'era']
5353

5454

5555
# Hankel Singular Value Decomposition
@@ -368,38 +368,129 @@ def minreal(sys, tol=None, verbose=True):
368368
return sysr
369369

370370

371-
def era(YY, m, n, nin, nout, r):
372-
"""Calculate an ERA model of order `r` based on the impulse-response data
371+
def _block_hankel(Y, m, n):
372+
"""Create a block Hankel matrix from impulse response"""
373+
q, p, _ = Y.shape
374+
YY = Y.transpose(0,2,1) # transpose for reshape
375+
376+
H = np.zeros((q*m,p*n))
377+
378+
for r in range(m):
379+
# shift and add row to Hankel matrix
380+
new_row = YY[:,r:r+n,:]
381+
H[q*r:q*(r+1),:] = new_row.reshape((q,p*n))
382+
383+
return H
384+
385+
386+
def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False):
387+
r"""eigensys_realization(YY, r)
388+
389+
Calculate an ERA model of order `r` based on the impulse-response data
373390
`YY`.
374391
375-
.. note:: This function is not implemented yet.
392+
This function computes a discrete time system
393+
394+
.. math::
395+
396+
x[k+1] &= A x[k] + B u[k] \\\\
397+
y[k] &= C x[k] + D u[k]
398+
399+
for a given impulse-response data (see [1]_).
400+
401+
The function can be called with 2 arguments:
402+
403+
* ``sysd, S = eigensys_realization(data, r)``
404+
* ``sysd, S = eigensys_realization(YY, r)``
405+
406+
where `data` is a `TimeResponseData` object, `YY` is a 1D or 3D
407+
array, and r is an integer.
376408
377409
Parameters
378410
----------
379-
YY: array
380-
`nout` x `nin` dimensional impulse-response data
381-
m: integer
382-
Number of rows in Hankel matrix
383-
n: integer
384-
Number of columns in Hankel matrix
385-
nin: integer
386-
Number of input variables
387-
nout: integer
388-
Number of output variables
389-
r: integer
390-
Order of model
411+
YY : array_like
412+
Impulse response from which the StateSpace model is estimated, 1D
413+
or 3D array.
414+
data : TimeResponseData
415+
Impulse response from which the StateSpace model is estimated.
416+
r : integer
417+
Order of model.
418+
m : integer, optional
419+
Number of rows in Hankel matrix. Default is 2*r.
420+
n : integer, optional
421+
Number of columns in Hankel matrix. Default is 2*r.
422+
dt : True or float, optional
423+
True indicates discrete time with unspecified sampling time and a
424+
positive float is discrete time with the specified sampling time.
425+
It can be used to scale the StateSpace model in order to match the
426+
unit-area impulse response of python-control. Default is True.
427+
transpose : bool, optional
428+
Assume that input data is transposed relative to the standard
429+
:ref:`time-series-convention`. For TimeResponseData this parameter
430+
is ignored. Default is False.
391431
392432
Returns
393433
-------
394-
sys: StateSpace
395-
A reduced order model sys=ss(Ar,Br,Cr,Dr)
434+
sys : StateSpace
435+
A reduced order model sys=StateSpace(Ar,Br,Cr,Dr,dt).
436+
S : array
437+
Singular values of Hankel matrix. Can be used to choose a good r
438+
value.
439+
440+
References
441+
----------
442+
.. [1] Samet Oymak and Necmiye Ozay, Non-asymptotic Identification of
443+
LTI Systems from a Single Trajectory.
444+
https://arxiv.org/abs/1806.05722
396445
397446
Examples
398447
--------
399-
>>> rsys = era(YY, m, n, nin, nout, r) # doctest: +SKIP
448+
>>> T = np.linspace(0, 10, 100)
449+
>>> _, YY = ct.impulse_response(ct.tf([1], [1, 0.5], True), T)
450+
>>> sysd, _ = ct.eigensys_realization(YY, r=1)
400451
452+
>>> T = np.linspace(0, 10, 100)
453+
>>> response = ct.impulse_response(ct.tf([1], [1, 0.5], True), T)
454+
>>> sysd, _ = ct.eigensys_realization(response, r=1)
401455
"""
402-
raise NotImplementedError('This function is not implemented yet.')
456+
if isinstance(arg, TimeResponseData):
457+
YY = np.array(arg.outputs, ndmin=3)
458+
if arg.transpose:
459+
YY = np.transpose(YY)
460+
else:
461+
YY = np.array(arg, ndmin=3)
462+
if transpose:
463+
YY = np.transpose(YY)
464+
465+
q, p, l = YY.shape
466+
467+
if m is None:
468+
m = 2*r
469+
if n is None:
470+
n = 2*r
471+
472+
if m*q < r or n*p < r:
473+
raise ValueError("Hankel parameters are to small")
474+
475+
if (l-1) < m+n:
476+
raise ValueError("not enough data for requested number of parameters")
477+
478+
H = _block_hankel(YY[:,:,1:], m, n+1) # Hankel matrix (q*m, p*(n+1))
479+
Hf = H[:,:-p] # first p*n columns of H
480+
Hl = H[:,p:] # last p*n columns of H
481+
482+
U,S,Vh = np.linalg.svd(Hf, True)
483+
Ur =U[:,0:r]
484+
Vhr =Vh[0:r,:]
485+
486+
# balanced realizations
487+
Sigma_inv = np.diag(1./np.sqrt(S[0:r]))
488+
Ar = Sigma_inv @ Ur.T @ Hl @ Vhr.T @ Sigma_inv
489+
Br = Sigma_inv @ Ur.T @ Hf[:,0:p]*dt # dt scaling for unit-area impulse
490+
Cr = Hf[0:q,:] @ Vhr.T @ Sigma_inv
491+
Dr = YY[:,:,0]
492+
493+
return StateSpace(Ar,Br,Cr,Dr,dt), S
403494

404495

405496
def markov(*args, m=None, transpose=False, dt=None, truncate=False):
@@ -593,3 +684,6 @@ def markov(*args, m=None, transpose=False, dt=None, truncate=False):
593684

594685
# Return the first m Markov parameters
595686
return H if not transpose else np.transpose(H)
687+
688+
# Function aliases
689+
era = eigensys_realization

control/tests/modelsimp_test.py

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
import numpy as np
77
import pytest
88

9-
10-
from control import StateSpace, forced_response, impulse_response, tf, rss, c2d, TimeResponseData
9+
from control import StateSpace, TimeResponseData, c2d, forced_response, \
10+
impulse_response, step_response, rss, tf
1111
from control.exception import ControlArgument, ControlDimension
12+
from control.modelsimp import balred, eigensys_realization, hsvd, markov, \
13+
modred
1214
from control.tests.conftest import slycotonly
13-
from control.modelsimp import balred, hsvd, markov, modred
1415

1516

1617
class TestModelsimp:
@@ -240,6 +241,85 @@ def testMarkovResults(self, k, m, n):
240241
np.testing.assert_allclose(Mtrue_scaled, Mcomp_scaled, rtol=1e-6, atol=1e-8)
241242

242243

244+
def testERASignature(self):
245+
246+
# test siso
247+
# Katayama, Subspace Methods for System Identification
248+
# Example 6.1, Fibonacci sequence
249+
H_true = np.array([0.,1.,1.,2.,3.,5.,8.,13.,21.,34.])
250+
251+
# A realization of fibonacci impulse response
252+
A = np.array([[0., 1.],[1., 1.,]])
253+
B = np.array([[1.],[1.,]])
254+
C = np.array([[1., 0.,]])
255+
D = np.array([[0.,]])
256+
257+
T = np.arange(0,10,1)
258+
sysd_true = StateSpace(A,B,C,D,True)
259+
ir_true = impulse_response(sysd_true,T=T)
260+
261+
# test TimeResponseData
262+
sysd_est, _ = eigensys_realization(ir_true,r=2)
263+
ir_est = impulse_response(sysd_est, T=T)
264+
_, H_est = ir_est
265+
266+
np.testing.assert_allclose(H_true, H_est, rtol=1e-6, atol=1e-8)
267+
268+
# test ndarray
269+
_, YY_true = ir_true
270+
sysd_est, _ = eigensys_realization(YY_true,r=2)
271+
ir_est = impulse_response(sysd_est, T=T)
272+
_, H_est = ir_est
273+
274+
np.testing.assert_allclose(H_true, H_est, rtol=1e-6, atol=1e-8)
275+
276+
# test mimo
277+
# Mechanical Vibrations: Theory and Application, SI Edition, 1st ed.
278+
# Figure 6.5 / Example 6.7
279+
# m q_dd + c q_d + k q = f
280+
m1, k1, c1 = 1., 4., 1.
281+
m2, k2, c2 = 2., 2., 1.
282+
k3, c3 = 6., 2.
283+
284+
A = np.array([
285+
[0., 0., 1., 0.],
286+
[0., 0., 0., 1.],
287+
[-(k1+k2)/m1, (k2)/m1, -(c1+c2)/m1, c2/m1],
288+
[(k2)/m2, -(k2+k3)/m2, c2/m2, -(c2+c3)/m2]
289+
])
290+
B = np.array([[0.,0.],[0.,0.],[1/m1,0.],[0.,1/m2]])
291+
C = np.array([[1.0, 0.0, 0.0, 0.0],[0.0, 1.0, 0.0, 0.0]])
292+
D = np.zeros((2,2))
293+
294+
sys = StateSpace(A, B, C, D)
295+
296+
dt = 0.1
297+
T = np.arange(0,10,dt)
298+
sysd_true = sys.sample(dt, method='zoh')
299+
ir_true = impulse_response(sysd_true, T=T)
300+
301+
# test TimeResponseData
302+
sysd_est, _ = eigensys_realization(ir_true,r=4,dt=dt)
303+
304+
step_true = step_response(sysd_true)
305+
step_est = step_response(sysd_est)
306+
307+
np.testing.assert_allclose(step_true.outputs,
308+
step_est.outputs,
309+
rtol=1e-6, atol=1e-8)
310+
311+
# test ndarray
312+
_, YY_true = ir_true
313+
sysd_est, _ = eigensys_realization(YY_true,r=4,dt=dt)
314+
315+
step_true = step_response(sysd_true, T=T)
316+
step_est = step_response(sysd_est, T=T)
317+
318+
np.testing.assert_allclose(step_true.outputs,
319+
step_est.outputs,
320+
rtol=1e-6, atol=1e-8)
321+
322+
243323
def testModredMatchDC(self):
244324
#balanced realization computed in matlab for the transfer function:
245325
# num = [1 11 45 32], den = [1 15 60 200 60]

doc/control.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ Model simplification tools
137137
balred
138138
hsvd
139139
modred
140-
era
140+
eigensys_realization
141141
markov
142142

143143
Nonlinear system support

doc/era_msd.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../examples/era_msd.py

doc/era_msd.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
ERA example, mass spring damper system
2+
--------------------------------------
3+
4+
Code
5+
....
6+
.. literalinclude:: era_msd.py
7+
:language: python
8+
:linenos:
9+
10+
11+
Notes
12+
.....
13+
14+
1. The environment variable `PYCONTROL_TEST_EXAMPLES` is used for
15+
testing to turn off plotting of the outputs.0

doc/examples.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ other sources.
3636
mrac_siso_mit
3737
mrac_siso_lyapunov
3838
markov
39+
era_msd
3940

4041
Jupyter notebooks
4142
=================

examples/era_msd.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# era_msd.py
2+
# Johannes Kaisinger, 4 July 2024
3+
#
4+
# Demonstrate estimation of State Space model from impulse response.
5+
# SISO, SIMO, MISO, MIMO case
6+
7+
import numpy as np
8+
import matplotlib.pyplot as plt
9+
import os
10+
11+
import control as ct
12+
13+
# set up a mass spring damper system (2dof, MIMO case)
14+
# Mechanical Vibrations: Theory and Application, SI Edition, 1st ed.
15+
# Figure 6.5 / Example 6.7
16+
# m q_dd + c q_d + k q = f
17+
m1, k1, c1 = 1., 4., 1.
18+
m2, k2, c2 = 2., 2., 1.
19+
k3, c3 = 6., 2.
20+
21+
A = np.array([
22+
[0., 0., 1., 0.],
23+
[0., 0., 0., 1.],
24+
[-(k1+k2)/m1, (k2)/m1, -(c1+c2)/m1, c2/m1],
25+
[(k2)/m2, -(k2+k3)/m2, c2/m2, -(c2+c3)/m2]
26+
])
27+
B = np.array([[0.,0.],[0.,0.],[1/m1,0.],[0.,1/m2]])
28+
C = np.array([[1.0, 0.0, 0.0, 0.0],[0.0, 1.0, 0.0, 0.0]])
29+
D = np.zeros((2,2))
30+
31+
xixo_list = ["SISO","SIMO","MISO","MIMO"]
32+
xixo = xixo_list[3] # choose a system for estimation
33+
match xixo:
34+
case "SISO":
35+
sys = ct.StateSpace(A, B[:,0], C[0,:], D[0,0])
36+
case "SIMO":
37+
sys = ct.StateSpace(A, B[:,:1], C, D[:,:1])
38+
case "MISO":
39+
sys = ct.StateSpace(A, B, C[:1,:], D[:1,:])
40+
case "MIMO":
41+
sys = ct.StateSpace(A, B, C, D)
42+
43+
44+
dt = 0.1
45+
sysd = sys.sample(dt, method='zoh')
46+
response = ct.impulse_response(sysd)
47+
response.plot()
48+
plt.show()
49+
50+
sysd_est, _ = ct.eigensys_realization(response,r=4,dt=dt)
51+
52+
step_true = ct.step_response(sysd)
53+
step_true.sysname="H_true"
54+
step_est = ct.step_response(sysd_est)
55+
step_est.sysname="H_est"
56+
57+
step_true.plot(title=xixo)
58+
step_est.plot(color='orange',linestyle='dashed')
59+
60+
plt.show()
61+
62+
if 'PYCONTROL_TEST_EXAMPLES' not in os.environ:
63+
plt.show()

0 commit comments

Comments
 (0)