Improve markov function, add mimo support, change api to TimeResponse… · python-control/python-control@ec38f2e

@@ -48,6 +48,7 @@

4848

from .iosys import isdtime, isctime

4949

from .statesp import StateSpace

5050

from .statefbk import gram

51+

from .timeresp import TimeResponseData

51525253

__all__ = ['hsvd', 'balred', 'modred', 'era', 'markov', 'minreal']

5354

@@ -402,9 +403,9 @@ def era(YY, m, n, nin, nout, r):

402403

raise NotImplementedError('This function is not implemented yet.')

403404404405405-

def markov(Y, U, m=None, transpose=False):

406+

def markov(data, m=None, dt=True, truncate=False):

406407

"""Calculate the first `m` Markov parameters [D CB CAB ...]

407-

from input `U`, output `Y`.

408+

from data

408409409410

This function computes the Markov parameters for a discrete time system

410411

@@ -420,23 +421,45 @@ def markov(Y, U, m=None, transpose=False):

420421421422

Parameters

422423

----------

423-

Y : array_like

424-

Output data. If the array is 1D, the system is assumed to be single

425-

input. If the array is 2D and transpose=False, the columns of `Y`

426-

are taken as time points, otherwise the rows of `Y` are taken as

427-

time points.

428-

U : array_like

429-

Input data, arranged in the same way as `Y`.

424+

data : TimeResponseData

425+

Response data from which the Markov parameters where estimated.

426+

Input and output data must be 1D or 2D array.

430427

m : int, optional

431428

Number of Markov parameters to output. Defaults to len(U).

432-

transpose : bool, optional

433-

Assume that input data is transposed relative to the standard

434-

:ref:`time-series-convention`. Default value is False.

429+

dt : (True of float, optional)

430+

True indicates discrete time with unspecified sampling time,

431+

positive number is discrete time with specified sampling time.

432+

It can be used to scale the markov parameters in order to match

433+

the impulse response of this library.

434+

Default values is True.

435+

truncate : bool, optional

436+

Do not use first m equation for least least squares.

437+

Default value is False.

435438436439

Returns

437440

-------

438-

H : ndarray

439-

First m Markov parameters, [D CB CAB ...]

441+

H : TimeResponseData

442+

Markov parameters / impulse response [D CB CAB ...] represented as

443+

a :class:`TimeResponseData` object containing the following properties:

444+445+

* time (array): Time values of the output.

446+447+

* outputs (array): Response of the system. If the system is SISO,

448+

the array is 1D (indexed by time). If the

449+

system is not SISO, the array is 3D (indexed

450+

by the output, trace, and time).

451+452+

* inputs (array): Inputs of the system. If the system is SISO,

453+

the array is 1D (indexed by time). If the

454+

system is not SISO, the array is 3D (indexed

455+

by the output, trace, and time).

456+457+

Notes

458+

-----

459+

It works for SISO and MIMO systems.

460+461+

This function does comply with the Python Control Library

462+

:ref:`time-series-convention` for representation of time series data.

440463441464

References

442465

----------

@@ -445,95 +468,69 @@ def markov(Y, U, m=None, transpose=False):

445468

and experiments. Journal of Guidance Control and Dynamics, 16(2),

446469

320-329, 2012. http://doi.org/10.2514/3.21006

447470448-

Notes

449-

-----

450-

Currently only works for SISO systems.

451-452-

This function does not currently comply with the Python Control Library

453-

:ref:`time-series-convention` for representation of time series data.

454-

Use `transpose=False` to make use of the standard convention (this

455-

will be updated in a future release).

456-457471

Examples

458472

--------

459473

>>> T = np.linspace(0, 10, 100)

460474

>>> U = np.ones((1, 100))

461-

>>> T, Y = ct.forced_response(ct.tf([1], [1, 0.5], True), T, U)

462-

>>> H = ct.markov(Y, U, 3, transpose=False)

475+

>>> response = ct.forced_response(ct.tf([1], [1, 0.5], True), T, U)

476+

>>> H = ct.markov(response, 3)

463477464478

"""

465479

# Convert input parameters to 2D arrays (if they aren't already)

466-

Umat = np.array(U, ndmin=2)

467-

Ymat = np.array(Y, ndmin=2)

480+

Umat = np.array(data.inputs, ndmin=2)

481+

Ymat = np.array(data.outputs, ndmin=2)

468482469483

# If data is in transposed format, switch it around

470-

if transpose:

484+

if data.transpose and not data.issiso:

471485

Umat, Ymat = np.transpose(Umat), np.transpose(Ymat)

472486473-

# Make sure the system is a SISO system

474-

if Umat.shape[0] != 1 or Ymat.shape[0] != 1:

475-

raise ControlMIMONotImplemented

476-477487

# Make sure the number of time points match

478488

if Umat.shape[1] != Ymat.shape[1]:

479489

raise ControlDimension(

480490

"Input and output data are of differnent lengths")

481-

n = Umat.shape[1]

491+

l = Umat.shape[1]

482492483493

# If number of desired parameters was not given, set to size of input data

484494

if m is None:

485-

m = Umat.shape[1]

495+

m = l

496+497+

t = 0

498+

if truncate:

499+

t = m

500+501+

q = Ymat.shape[0] # number of outputs

502+

p = Umat.shape[0] # number of inputs

486503487504

# Make sure there is enough data to compute parameters

488-

if m > n:

505+

if m*p > (l-t):

489506

warnings.warn("Not enough data for requested number of parameters")

490507508+

# the algorithm - Construct a matrix of control inputs to invert

491509

#

492-

# Original algorithm (with mapping to standard order)

493-

#

494-

# RMM note, 24 Dec 2020: This algorithm sets the problem up correctly

495-

# until the final column of the UU matrix is created, at which point it

496-

# makes some modifications that I don't understand. This version of the

497-

# algorithm does not seem to return the actual Markov parameters for a

498-

# system.

499-

#

500-

# # Create the matrix of (shifted) inputs

501-

# UU = np.transpose(Umat)

502-

# for i in range(1, m-1):

503-

# # Shift previous column down and add a zero at the top

504-

# newCol = np.vstack((0, np.reshape(UU[0:n-1, i-1], (-1, 1))))

505-

# UU = np.hstack((UU, newCol))

506-

#

507-

# # Shift previous column down and add a zero at the top

508-

# Ulast = np.vstack((0, np.reshape(UU[0:n-1, m-2], (-1, 1))))

509-

#

510-

# # Replace the elements of the last column new values (?)

511-

# # Each row gets the sum of the rows above it (?)

512-

# for i in range(n-1, 0, -1):

513-

# Ulast[i] = np.sum(Ulast[0:i-1])

514-

# UU = np.hstack((UU, Ulast))

515-

#

516-

# # Solve for the Markov parameters from Y = H @ UU

517-

# # H = [[D], [CB], [CAB], ..., [C A^{m-3} B], [???]]

518-

# H = np.linalg.lstsq(UU, np.transpose(Ymat))[0]

519-

#

520-

# # Markov parameters are in rows => transpose if needed

521-

# return H if transpose else np.transpose(H)

522-523-

#

524-

# New algorithm - Construct a matrix of control inputs to invert

510+

# (q,l) = (q,p*m) @ (p*m,l)

511+

# YY.T = H @ UU.T

525512

#

526513

# This algorithm sets up the following problem and solves it for

527514

# the Markov parameters

528515

#

516+

# (l,q) = (l,p*m) @ (p*m,q)

517+

# YY = UU @ H.T

518+

#

529519

# [ y(0) ] [ u(0) 0 0 ] [ D ]

530520

# [ y(1) ] [ u(1) u(0) 0 ] [ C B ]

531521

# [ y(2) ] = [ u(2) u(1) u(0) ] [ C A B ]

532522

# [ : ] [ : : : : ] [ : ]

533-

# [ y(n-1) ] [ u(n-1) u(n-2) u(n-3) ... u(n-m) ] [ C A^{m-2} B ]

523+

# [ y(l-1) ] [ u(l-1) u(l-2) u(l-3) ... u(l-m) ] [ C A^{m-2} B ]

534524

#

535-

# Note: if the number of Markov parameters (m) is less than the size of

536-

# the input/output data (n), then this algorithm assumes C A^{j} B = 0

525+

# truncated version t=m, do not use first m equation

526+

#

527+

# [ y(t) ] [ u(t) u(t-1) u(t-2) u(t-m) ] [ D ]

528+

# [ y(t+1) ] [ u(t+1) u(t) u(t-1) u(t-m+1)] [ C B ]

529+

# [ y(t+2) ] = [ u(t+2) u(t+1) u(t) u(t-m+2)] [ C B ]

530+

# [ : ] [ : : : : ] [ : ]

531+

# [ y(l-1) ] [ u(l-1) u(l-2) u(l-3) ... u(l-m) ] [ C A^{m-2} B ]

532+

#

533+

# Note: This algorithm assumes C A^{j} B = 0

537534

# for j > m-2. See equation (3) in

538535

#

539536

# J.-N. Juang, M. Phan, L. G. Horta, and R. W. Longman, Identification

@@ -542,17 +539,40 @@ def markov(Y, U, m=None, transpose=False):

542539

# 320-329, 2012. http://doi.org/10.2514/3.21006

543540

#

544541542+

# Set up the full problem

545543

# Create matrix of (shifted) inputs

546-

UU = Umat

547-

for i in range(1, m):

548-

# Shift previous column down and add a zero at the top

549-

new_row = np.hstack((0, UU[i-1, 0:-1]))

550-

UU = np.vstack((UU, new_row))

551-

UU = np.transpose(UU)

552-553-

# Invert and solve for Markov parameters

554-

YY = np.transpose(Ymat)

555-

H, _, _, _ = np.linalg.lstsq(UU, YY, rcond=None)

556-544+

UUT = np.zeros((p*m,(l)))

545+

for i in range(m):

546+

# Shift previous column down and keep zeros at the top

547+

UUT[i*p:(i+1)*p,i:] = Umat[:,:l-i]

548+549+

# Truncate first t=0 or t=m time steps, transpose the problem for lsq

550+

YY = Ymat[:,t:].T

551+

UU = UUT[:,t:].T

552+553+

# Solve for the Markov parameters from YY = UU @ H.T

554+

HT, _, _, _ = np.linalg.lstsq(UU, YY, rcond=None)

555+

H = HT.T/dt # scaling

556+557+

H = H.reshape(q,m,p) # output, time*input -> output, time, input

558+

H = H.transpose(0,2,1) # output, input, time

559+560+

# Create unit area impulse inputs

561+

inputs = np.zeros((q,p,m))

562+

trace_labels, trace_types = [], []

563+

for i in range(p):

564+

inputs[i,i,0] = 1/dt # unit area impulse

565+

trace_labels.append(f"From {data.input_labels[i]}")

566+

trace_types.append('impulse')

567+568+

# Markov parameters as TimeResponseData with unit area impulse inputs

557569

# Return the first m Markov parameters

558-

return H if transpose else np.transpose(H)

570+

return TimeResponseData(time=data.time[:m],

571+

outputs=H,

572+

output_labels=data.output_labels,

573+

inputs=inputs,

574+

input_labels=data.input_labels,

575+

trace_labels=trace_labels,

576+

trace_types=trace_types,

577+

transpose=data.transpose,

578+

issiso=data.issiso)