Improve markov function, add mimo support, change api to TimeResponse… · python-control/python-control@ec38f2e
@@ -48,6 +48,7 @@
4848from .iosys import isdtime, isctime
4949from .statesp import StateSpace
5050from .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):
402403raise 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:
471485Umat, 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
478488if Umat.shape[1] != Ymat.shape[1]:
479489raise 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
484494if 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):
489506warnings.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)