|
49 | 49 | from .statefbk import gram |
50 | 50 | from .timeresp import TimeResponseData |
51 | 51 |
|
52 | | -__all__ = ['hsvd', 'balred', 'modred', 'era', 'markov', 'minreal'] |
| 52 | +__all__ = ['hsvd', 'balred', 'modred', 'eigensys_realization', 'markov', 'minreal', 'era'] |
53 | 53 |
|
54 | 54 |
|
55 | 55 | # Hankel Singular Value Decomposition |
@@ -368,38 +368,129 @@ def minreal(sys, tol=None, verbose=True): |
368 | 368 | return sysr |
369 | 369 |
|
370 | 370 |
|
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 |
373 | 390 | `YY`. |
374 | 391 |
|
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. |
376 | 408 |
|
377 | 409 | Parameters |
378 | 410 | ---------- |
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. |
391 | 431 |
|
392 | 432 | Returns |
393 | 433 | ------- |
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 |
396 | 445 |
|
397 | 446 | Examples |
398 | 447 | -------- |
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) |
400 | 451 |
|
| 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) |
401 | 455 | """ |
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 |
403 | 494 |
|
404 | 495 |
|
405 | 496 | 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): |
593 | 684 |
|
594 | 685 | # Return the first m Markov parameters |
595 | 686 | return H if not transpose else np.transpose(H) |
| 687 | + |
| 688 | +# Function aliases |
| 689 | +era = eigensys_realization |
0 commit comments