4848from .iosys import isdtime , isctime
4949from .statesp import StateSpace
5050from .statefbk import gram
51+ from .timeresp import TimeResponseData
5152
5253__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.' )
403404
404405
405- 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
408409
409410 This function computes the Markov parameters for a discrete time system
410411
@@ -420,23 +421,45 @@ def markov(Y, U, m=None, transpose=False):
420421
421422 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.
435438
436439 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.
440463
441464 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
447470
448- 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 )
463477
464478 """
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 )
468482
469483 # 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 )
472486
473- # 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 ]
482492
483493 # 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
486503
487504 # 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" )
490507
508+ # 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 #
544541
542+ # 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 )
0 commit comments