• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

python-control / python-control / 10267058838

06 Aug 2024 01:01PM UTC coverage: 94.65% (+0.002%) from 94.648%
10267058838

push

github

murrayrm
Merge branch 'KybernetikJo-modelsimp_full_name'

8987 of 9495 relevant lines covered (94.65%)

8.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.46
control/modelsimp.py
1
#! TODO: add module docstring
2
# modelsimp.py - tools for model simplification
3
#
4
# Author: Steve Brunton, Kevin Chen, Lauren Padilla
5
# Date: 30 Nov 2010
6
#
7
# This file contains routines for obtaining reduced order models
8
#
9
# Copyright (c) 2010 by California Institute of Technology
10
# All rights reserved.
11
#
12
# Redistribution and use in source and binary forms, with or without
13
# modification, are permitted provided that the following conditions
14
# are met:
15
#
16
# 1. Redistributions of source code must retain the above copyright
17
#    notice, this list of conditions and the following disclaimer.
18
#
19
# 2. Redistributions in binary form must reproduce the above copyright
20
#    notice, this list of conditions and the following disclaimer in the
21
#    documentation and/or other materials provided with the distribution.
22
#
23
# 3. Neither the name of the California Institute of Technology nor
24
#    the names of its contributors may be used to endorse or promote
25
#    products derived from this software without specific prior
26
#    written permission.
27
#
28
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
31
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL CALTECH
32
# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
33
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
34
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
35
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
36
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
37
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
38
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
39
# SUCH DAMAGE.
40
#
41
# $Id$
42

43
# External packages and modules
44
import numpy as np
9✔
45
import warnings
9✔
46
from .exception import ControlSlycot, ControlArgument, ControlDimension
9✔
47
from .iosys import isdtime, isctime
9✔
48
from .statesp import StateSpace
9✔
49
from .statefbk import gram
9✔
50
from .timeresp import TimeResponseData
9✔
51

52
__all__ = ['hankel_singular_values', 'balanced_reduction', 'model_reduction',
9✔
53
           'minimal_realization', 'eigensys_realization', 'markov', 'hsvd',
54
           'balred', 'modred', 'minreal', 'era']
55

56

57
# Hankel Singular Value Decomposition
58
#
59
# The following returns the Hankel singular values, which are singular values
60
# of the matrix formed by multiplying the controllability and observability
61
# Gramians
62
def hankel_singular_values(sys):
9✔
63
    """Calculate the Hankel singular values.
64

65
    Parameters
66
    ----------
67
    sys : StateSpace
68
        A state space system
69

70
    Returns
71
    -------
72
    H : array
73
        A list of Hankel singular values
74

75
    See Also
76
    --------
77
    gram
78

79
    Notes
80
    -----
81
    The Hankel singular values are the singular values of the Hankel operator.
82
    In practice, we compute the square root of the eigenvalues of the matrix
83
    formed by taking the product of the observability and controllability
84
    gramians.  There are other (more efficient) methods based on solving the
85
    Lyapunov equation in a particular way (more details soon).
86

87
    Examples
88
    --------
89
    >>> G = ct.tf2ss([1], [1, 2])
90
    >>> H = ct.hsvd(G)
91
    >>> H[0]
92
    np.float64(0.25)
93

94
    """
95
    # TODO: implement for discrete time systems
96
    if (isdtime(sys, strict=True)):
5✔
97
        raise NotImplementedError("Function not implemented in discrete time")
98

99
    Wc = gram(sys, 'c')
5✔
100
    Wo = gram(sys, 'o')
5✔
101
    WoWc = Wo @ Wc
5✔
102
    w, v = np.linalg.eig(WoWc)
5✔
103

104
    hsv = np.sqrt(w)
5✔
105
    hsv = np.array(hsv)
5✔
106
    hsv = np.sort(hsv)
5✔
107
    # Return the Hankel singular values, high to low
108
    return hsv[::-1]
5✔
109

110

111
def model_reduction(sys, ELIM, method='matchdc'):
9✔
112
    """
113
    Model reduction of `sys` by eliminating the states in `ELIM` using a given
114
    method.
115

116
    Parameters
117
    ----------
118
    sys: StateSpace
119
        Original system to reduce
120
    ELIM: array
121
        Vector of states to eliminate
122
    method: string
123
        Method of removing states in `ELIM`: either ``'truncate'`` or
124
        ``'matchdc'``.
125

126
    Returns
127
    -------
128
    rsys: StateSpace
129
        A reduced order model
130

131
    Raises
132
    ------
133
    ValueError
134
        Raised under the following conditions:
135

136
            * if `method` is not either ``'matchdc'`` or ``'truncate'``
137

138
            * if eigenvalues of `sys.A` are not all in left half plane
139
              (`sys` must be stable)
140

141
    Examples
142
    --------
143
    >>> G = ct.rss(4)
144
    >>> Gr = ct.modred(G, [0, 2], method='matchdc')
145
    >>> Gr.nstates
146
    2
147

148
    """
149

150
    # Check for ss system object, need a utility for this?
151

152
    # TODO: Check for continous or discrete, only continuous supported for now
153
    #   if isCont():
154
    #       dico = 'C'
155
    #   elif isDisc():
156
    #       dico = 'D'
157
    #   else:
158
    if (isctime(sys)):
9✔
159
        dico = 'C'
9✔
160
    else:
161
        raise NotImplementedError("Function not implemented in discrete time")
162

163
    # Check system is stable
164
    if np.any(np.linalg.eigvals(sys.A).real >= 0.0):
9✔
165
        raise ValueError("Oops, the system is unstable!")
9✔
166

167
    ELIM = np.sort(ELIM)
9✔
168
    # Create list of elements not to eliminate (NELIM)
169
    NELIM = [i for i in range(len(sys.A)) if i not in ELIM]
9✔
170
    # A1 is a matrix of all columns of sys.A not to eliminate
171
    A1 = sys.A[:, NELIM[0]].reshape(-1, 1)
9✔
172
    for i in NELIM[1:]:
9✔
173
        A1 = np.hstack((A1, sys.A[:, i].reshape(-1, 1)))
9✔
174
    A11 = A1[NELIM, :]
9✔
175
    A21 = A1[ELIM, :]
9✔
176
    # A2 is a matrix of all columns of sys.A to eliminate
177
    A2 = sys.A[:, ELIM[0]].reshape(-1, 1)
9✔
178
    for i in ELIM[1:]:
9✔
179
        A2 = np.hstack((A2, sys.A[:, i].reshape(-1, 1)))
9✔
180
    A12 = A2[NELIM, :]
9✔
181
    A22 = A2[ELIM, :]
9✔
182

183
    C1 = sys.C[:, NELIM]
9✔
184
    C2 = sys.C[:, ELIM]
9✔
185
    B1 = sys.B[NELIM, :]
9✔
186
    B2 = sys.B[ELIM, :]
9✔
187

188
    if method == 'matchdc':
9✔
189
        # if matchdc, residualize
190

191
        # Check if the matrix A22 is invertible
192
        if np.linalg.matrix_rank(A22) != len(ELIM):
9✔
193
            raise ValueError("Matrix A22 is singular to working precision.")
×
194

195
        # Now precompute A22\A21 and A22\B2 (A22I = inv(A22))
196
        # We can solve two linear systems in one pass, since the
197
        # coefficients matrix A22 is the same. Thus, we perform the LU
198
        # decomposition (cubic runtime complexity) of A22 only once!
199
        # The remaining back substitutions are only quadratic in runtime.
200
        A22I_A21_B2 = np.linalg.solve(A22, np.concatenate((A21, B2), axis=1))
9✔
201
        A22I_A21 = A22I_A21_B2[:, :A21.shape[1]]
9✔
202
        A22I_B2 = A22I_A21_B2[:, A21.shape[1]:]
9✔
203

204
        Ar = A11 - A12 @ A22I_A21
9✔
205
        Br = B1 - A12 @ A22I_B2
9✔
206
        Cr = C1 - C2 @ A22I_A21
9✔
207
        Dr = sys.D - C2 @ A22I_B2
9✔
208
    elif method == 'truncate':
9✔
209
        # if truncate, simply discard state x2
210
        Ar = A11
9✔
211
        Br = B1
9✔
212
        Cr = C1
9✔
213
        Dr = sys.D
9✔
214
    else:
215
        raise ValueError("Oops, method is not supported!")
×
216

217
    rsys = StateSpace(Ar, Br, Cr, Dr)
9✔
218
    return rsys
9✔
219

220

221
def balanced_reduction(sys, orders, method='truncate', alpha=None):
9✔
222
    """Balanced reduced order model of sys of a given order.
223
    States are eliminated based on Hankel singular value.
224
    If sys has unstable modes, they are removed, the
225
    balanced realization is done on the stable part, then
226
    reinserted in accordance with the reference below.
227

228
    Reference: Hsu,C.S., and Hou,D., 1991,
229
    Reducing unstable linear control systems via real Schur transformation.
230
    Electronics Letters, 27, 984-986.
231

232
    Parameters
233
    ----------
234
    sys: StateSpace
235
        Original system to reduce
236
    orders: integer or array of integer
237
        Desired order of reduced order model (if a vector, returns a vector
238
        of systems)
239
    method: string
240
        Method of removing states, either ``'truncate'`` or ``'matchdc'``.
241
    alpha: float
242
        Redefines the stability boundary for eigenvalues of the system
243
        matrix A.  By default for continuous-time systems, alpha <= 0
244
        defines the stability boundary for the real part of A's eigenvalues
245
        and for discrete-time systems, 0 <= alpha <= 1 defines the stability
246
        boundary for the modulus of A's eigenvalues. See SLICOT routines
247
        AB09MD and AB09ND for more information.
248

249
    Returns
250
    -------
251
    rsys: StateSpace
252
        A reduced order model or a list of reduced order models if orders is
253
        a list.
254

255
    Raises
256
    ------
257
    ValueError
258
        If `method` is not ``'truncate'`` or ``'matchdc'``
259
    ImportError
260
        if slycot routine ab09ad, ab09md, or ab09nd is not found
261

262
    ValueError
263
        if there are more unstable modes than any value in orders
264

265
    Examples
266
    --------
267
    >>> G = ct.rss(4)
268
    >>> Gr = ct.balred(G, orders=2, method='matchdc')
269
    >>> Gr.nstates
270
    2
271

272
    """
273
    if method != 'truncate' and method != 'matchdc':
5✔
274
        raise ValueError("supported methods are 'truncate' or 'matchdc'")
×
275
    elif method == 'truncate':
5✔
276
        try:
5✔
277
            from slycot import ab09md, ab09ad
5✔
278
        except ImportError:
×
279
            raise ControlSlycot(
×
280
                "can't find slycot subroutine ab09md or ab09ad")
281
    elif method == 'matchdc':
5✔
282
        try:
5✔
283
            from slycot import ab09nd
5✔
284
        except ImportError:
×
285
            raise ControlSlycot("can't find slycot subroutine ab09nd")
×
286

287
    # Check for ss system object, need a utility for this?
288

289
    # TODO: Check for continous or discrete, only continuous supported for now
290
    #   if isCont():
291
    #       dico = 'C'
292
    #   elif isDisc():
293
    #       dico = 'D'
294
    #   else:
295
    dico = 'C'
5✔
296

297
    job = 'B'                   # balanced (B) or not (N)
5✔
298
    equil = 'N'                 # scale (S) or not (N)
5✔
299
    if alpha is None:
5✔
300
        if dico == 'C':
5✔
301
            alpha = 0.
5✔
302
        elif dico == 'D':
×
303
            alpha = 1.
×
304

305
    rsys = []                   # empty list for reduced systems
5✔
306

307
    # check if orders is a list or a scalar
308
    try:
5✔
309
        order = iter(orders)
5✔
310
    except TypeError:           # if orders is a scalar
5✔
311
        orders = [orders]
5✔
312

313
    for i in orders:
5✔
314
        n = np.size(sys.A, 0)
5✔
315
        m = np.size(sys.B, 1)
5✔
316
        p = np.size(sys.C, 0)
5✔
317
        if method == 'truncate':
5✔
318
            # check system stability
319
            if np.any(np.linalg.eigvals(sys.A).real >= 0.0):
5✔
320
                # unstable branch
321
                Nr, Ar, Br, Cr, Ns, hsv = ab09md(
×
322
                    dico, job, equil, n, m, p, sys.A, sys.B, sys.C,
323
                    alpha=alpha, nr=i, tol=0.0)
324
            else:
325
                # stable branch
326
                Nr, Ar, Br, Cr, hsv = ab09ad(
5✔
327
                    dico, job, equil, n, m, p, sys.A, sys.B, sys.C,
328
                    nr=i, tol=0.0)
329
            rsys.append(StateSpace(Ar, Br, Cr, sys.D))
5✔
330

331
        elif method == 'matchdc':
5✔
332
            Nr, Ar, Br, Cr, Dr, Ns, hsv = ab09nd(
5✔
333
                dico, job, equil, n, m, p, sys.A, sys.B, sys.C, sys.D,
334
                alpha=alpha, nr=i, tol1=0.0, tol2=0.0)
335
            rsys.append(StateSpace(Ar, Br, Cr, Dr))
5✔
336

337
    # if orders was a scalar, just return the single reduced model, not a list
338
    if len(orders) == 1:
5✔
339
        return rsys[0]
5✔
340
    # if orders was a list/vector, return a list/vector of systems
341
    else:
342
        return rsys
5✔
343

344

345
def minimal_realization(sys, tol=None, verbose=True):
9✔
346
    '''
347
    Eliminates uncontrollable or unobservable states in state-space
348
    models or cancelling pole-zero pairs in transfer functions. The
349
    output sysr has minimal order and the same response
350
    characteristics as the original model sys.
351

352
    Parameters
353
    ----------
354
    sys: StateSpace or TransferFunction
355
        Original system
356
    tol: real
357
        Tolerance
358
    verbose: bool
359
        Print results if True
360

361
    Returns
362
    -------
363
    rsys: StateSpace or TransferFunction
364
        Cleaned model
365
    '''
366
    sysr = sys.minreal(tol)
5✔
367
    if verbose:
5✔
368
        print("{nstates} states have been removed from the model".format(
5✔
369
                nstates=len(sys.poles()) - len(sysr.poles())))
370
    return sysr
5✔
371

372

373
def _block_hankel(Y, m, n):
9✔
374
    """Create a block Hankel matrix from impulse response"""
375
    q, p, _ = Y.shape
9✔
376
    YY = Y.transpose(0,2,1) # transpose for reshape
9✔
377
    
378
    H = np.zeros((q*m,p*n))
9✔
379
    
380
    for r in range(m):
9✔
381
        # shift and add row to Hankel matrix
382
        new_row = YY[:,r:r+n,:]
9✔
383
        H[q*r:q*(r+1),:] = new_row.reshape((q,p*n))
9✔
384
            
385
    return H
9✔
386

387

388
def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False):
9✔
389
    r"""eigensys_realization(YY, r)
390

391
    Calculate ERA model of order `r` based on impulse-response data `YY`.
392

393
    This function computes a discrete time system
394

395
    .. math::
396

397
        x[k+1] &= A x[k] + B u[k] \\\\
398
        y[k] &= C x[k] + D u[k]
399

400
    for a given impulse-response data (see [1]_).
401

402
    The function can be called with 2 arguments:
403

404
    * ``sysd, S = eigensys_realization(data, r)``
405
    * ``sysd, S = eigensys_realization(YY, r)``
406

407
    where `data` is a `TimeResponseData` object, `YY` is a 1D or 3D
408
    array, and r is an integer.
409

410
    Parameters
411
    ----------
412
    YY : array_like
413
        Impulse response from which the StateSpace model is estimated, 1D
414
        or 3D array.
415
    data : TimeResponseData
416
        Impulse response from which the StateSpace model is estimated.
417
    r : integer
418
        Order of model.
419
    m : integer, optional
420
        Number of rows in Hankel matrix. Default is 2*r.
421
    n : integer, optional
422
        Number of columns in Hankel matrix. Default is 2*r.
423
    dt : True or float, optional
424
        True indicates discrete time with unspecified sampling time and a
425
        positive float is discrete time with the specified sampling time.
426
        It can be used to scale the StateSpace model in order to match the
427
        unit-area impulse response of python-control. Default is True.
428
    transpose : bool, optional
429
        Assume that input data is transposed relative to the standard
430
        :ref:`time-series-convention`. For TimeResponseData this parameter 
431
        is ignored. Default is False.
432

433
    Returns
434
    -------
435
    sys : StateSpace
436
        A reduced order model sys=StateSpace(Ar,Br,Cr,Dr,dt).
437
    S : array
438
        Singular values of Hankel matrix. Can be used to choose a good r
439
        value.
440

441
    References
442
    ----------
443
    .. [1] Samet Oymak and Necmiye Ozay, Non-asymptotic Identification of
444
       LTI Systems from a Single Trajectory.
445
       https://arxiv.org/abs/1806.05722
446

447
    Examples
448
    --------
449
    >>> T = np.linspace(0, 10, 100)
450
    >>> _, YY = ct.impulse_response(ct.tf([1], [1, 0.5], True), T)
451
    >>> sysd, _ = ct.eigensys_realization(YY, r=1)
452

453
    >>> T = np.linspace(0, 10, 100)
454
    >>> response = ct.impulse_response(ct.tf([1], [1, 0.5], True), T)
455
    >>> sysd, _ = ct.eigensys_realization(response, r=1)
456
    """
457
    if isinstance(arg, TimeResponseData):
9✔
458
        YY = np.array(arg.outputs, ndmin=3)
9✔
459
        if arg.transpose:
9✔
460
            YY = np.transpose(YY)
×
461
    else:
462
        YY = np.array(arg, ndmin=3)
9✔
463
        if transpose:
9✔
464
            YY = np.transpose(YY)
×
465
    
466
    q, p, l = YY.shape
9✔
467

468
    if m is None:
9✔
469
        m = 2*r
9✔
470
    if n is None:
9✔
471
        n = 2*r
9✔
472

473
    if m*q < r or n*p < r:
9✔
474
        raise ValueError("Hankel parameters are to small")
×
475
    
476
    if (l-1) < m+n:
9✔
477
        raise ValueError("not enough data for requested number of parameters")
×
478
    
479
    H = _block_hankel(YY[:,:,1:], m, n+1) # Hankel matrix (q*m, p*(n+1))
9✔
480
    Hf = H[:,:-p] # first p*n columns of H
9✔
481
    Hl = H[:,p:] # last p*n columns of H
9✔
482
    
483
    U,S,Vh = np.linalg.svd(Hf, True)
9✔
484
    Ur =U[:,0:r]
9✔
485
    Vhr =Vh[0:r,:]
9✔
486

487
    # balanced realizations
488
    Sigma_inv = np.diag(1./np.sqrt(S[0:r]))
9✔
489
    Ar = Sigma_inv @ Ur.T @ Hl @ Vhr.T @ Sigma_inv
9✔
490
    Br = Sigma_inv @ Ur.T @ Hf[:,0:p]*dt # dt scaling for unit-area impulse
9✔
491
    Cr = Hf[0:q,:] @ Vhr.T @ Sigma_inv
9✔
492
    Dr = YY[:,:,0]
9✔
493

494
    return StateSpace(Ar,Br,Cr,Dr,dt), S
9✔
495

496

497
def markov(*args, m=None, transpose=False, dt=None, truncate=False):
9✔
498
    """markov(Y, U, [, m])
499
    
500
    Calculate the first `m` Markov parameters [D CB CAB ...] from data.
501

502
    This function computes the Markov parameters for a discrete time
503
    system
504

505
    .. math::
506

507
        x[k+1] &= A x[k] + B u[k] \\\\
508
        y[k] &= C x[k] + D u[k]
509

510
    given data for u and y.  The algorithm assumes that that C A^k B = 0
511
    for k > m-2 (see [1]_).  Note that the problem is ill-posed if the
512
    length of the input data is less than the desired number of Markov
513
    parameters (a warning message is generated in this case).
514

515
    The function can be called with either 1, 2 or 3 arguments:
516

517
    * ``H = markov(data)``
518
    * ``H = markov(data, m)``
519
    * ``H = markov(Y, U)``
520
    * ``H = markov(Y, U, m)``
521

522
    where `data` is a `TimeResponseData` object, `YY` is a 1D or 3D
523
    array, and r is an integer.
524

525
    Parameters
526
    ----------
527
    Y : array_like
528
        Output data. If the array is 1D, the system is assumed to be
529
        single input. If the array is 2D and transpose=False, the columns
530
        of `Y` are taken as time points, otherwise the rows of `Y` are
531
        taken as time points.
532
    U : array_like
533
        Input data, arranged in the same way as `Y`.
534
    data : TimeResponseData
535
        Response data from which the Markov parameters where estimated.
536
        Input and output data must be 1D or 2D array.
537
    m : int, optional
538
        Number of Markov parameters to output. Defaults to len(U).
539
    dt : True of float, optional
540
        True indicates discrete time with unspecified sampling time and a
541
        positive float is discrete time with the specified sampling time.
542
        It can be used to scale the Markov parameters in order to match
543
        the unit-area impulse response of python-control. Default is True
544
        for array_like and dt=data.time[1]-data.time[0] for
545
        TimeResponseData as input.
546
    truncate : bool, optional
547
        Do not use first m equation for least squares. Default is False.
548
    transpose : bool, optional
549
        Assume that input data is transposed relative to the standard
550
        :ref:`time-series-convention`. For TimeResponseData this parameter
551
        is ignored. Default is False.
552

553
    Returns
554
    -------
555
    H : ndarray
556
        First m Markov parameters, [D CB CAB ...].
557

558
    References
559
    ----------
560
    .. [1] J.-N. Juang, M. Phan, L. G.  Horta, and R. W. Longman,
561
       Identification of observer/Kalman filter Markov parameters - Theory
562
       and experiments. Journal of Guidance Control and Dynamics, 16(2),
563
       320-329, 2012. http://doi.org/10.2514/3.21006
564

565
    Examples
566
    --------
567
    >>> T = np.linspace(0, 10, 100)
568
    >>> U = np.ones((1, 100))
569
    >>> T, Y = ct.forced_response(ct.tf([1], [1, 0.5], True), T, U)
570
    >>> H = ct.markov(Y, U, 3, transpose=False)
571

572
    """
573

574
    # Convert input parameters to 2D arrays (if they aren't already)
575
    # Get the system description
576
    if len(args) < 1:
9✔
577
        raise ControlArgument("not enough input arguments")
9✔
578
    
579
    if isinstance(args[0], TimeResponseData):
9✔
580
        data = args[0]
9✔
581
        Umat = np.array(data.inputs, ndmin=2)
9✔
582
        Ymat = np.array(data.outputs, ndmin=2)
9✔
583
        if dt is None:
9✔
584
            dt = data.time[1] - data.time[0]
9✔
585
            if not np.allclose(np.diff(data.time), dt):
9✔
586
                raise ValueError("response time values must be equally "
×
587
                                 "spaced.")
588
        transpose = data.transpose
9✔
589
        if data.transpose and not data.issiso:
9✔
590
            Umat, Ymat = np.transpose(Umat), np.transpose(Ymat)
9✔
591
        if len(args) == 2:
9✔
592
            m = args[1]
9✔
593
        elif len(args) > 2:
9✔
594
            raise ControlArgument("too many positional arguments")
9✔
595
    else:
596
        if len(args) < 2:
9✔
597
            raise ControlArgument("not enough input arguments")
9✔
598
        Umat = np.array(args[1], ndmin=2)
9✔
599
        Ymat = np.array(args[0], ndmin=2)
9✔
600
        if dt is None:
9✔
601
            dt = True
9✔
602
        if transpose:
9✔
603
            Umat, Ymat = np.transpose(Umat), np.transpose(Ymat)
9✔
604
        if len(args) == 3:
9✔
605
            m = args[2]
9✔
606
        elif len(args) > 3:
9✔
607
            raise ControlArgument("too many positional arguments")
9✔
608

609
    # Make sure the number of time points match
610
    if Umat.shape[1] != Ymat.shape[1]:
9✔
611
        raise ControlDimension(
9✔
612
            "Input and output data are of differnent lengths")
613
    l = Umat.shape[1]
9✔
614

615
    # If number of desired parameters was not given, set to size of input data
616
    if m is None:
9✔
617
        m = l
9✔
618

619
    t = 0
9✔
620
    if truncate:
9✔
621
        t = m
9✔
622

623
    q = Ymat.shape[0] # number of outputs
9✔
624
    p = Umat.shape[0] # number of inputs
9✔
625

626
    # Make sure there is enough data to compute parameters
627
    if m*p > (l-t):
9✔
628
        warnings.warn("Not enough data for requested number of parameters")
9✔
629

630
    # the algorithm - Construct a matrix of control inputs to invert
631
    #
632
    # (q,l)   = (q,p*m) @ (p*m,l)
633
    # YY.T    = H @ UU.T
634
    #
635
    # This algorithm sets up the following problem and solves it for
636
    # the Markov parameters
637
    #
638
    # (l,q)   = (l,p*m) @ (p*m,q) 
639
    # YY      = UU @ H.T
640
    #
641
    # [ y(0)   ]   [ u(0)    0       0                 ] [ D           ]
642
    # [ y(1)   ]   [ u(1)    u(0)    0                 ] [ C B         ]
643
    # [ y(2)   ] = [ u(2)    u(1)    u(0)              ] [ C A B       ]
644
    # [  :     ]   [  :      :        :          :     ] [  :          ]
645
    # [ y(l-1) ]   [ u(l-1)  u(l-2)  u(l-3) ... u(l-m) ] [ C A^{m-2} B ]
646
    #
647
    # truncated version t=m, do not use first m equation
648
    #
649
    # [ y(t)   ]   [ u(t)    u(t-1)  u(t-2)    u(t-m)  ] [ D           ]
650
    # [ y(t+1) ]   [ u(t+1)  u(t)    u(t-1)    u(t-m+1)] [ C B         ]
651
    # [ y(t+2) ] = [ u(t+2)  u(t+1)  u(t)      u(t-m+2)] [ C B         ]
652
    # [  :     ]   [  :      :        :          :     ] [  :          ]
653
    # [ y(l-1) ]   [ u(l-1)  u(l-2)  u(l-3) ... u(l-m) ] [ C A^{m-2} B ]
654
    #
655
    # Note: This algorithm assumes C A^{j} B = 0
656
    # for j > m-2.  See equation (3) in
657
    #
658
    #   J.-N. Juang, M. Phan, L. G. Horta, and R. W. Longman, Identification
659
    #   of observer/Kalman filter Markov parameters - Theory and
660
    #   experiments. Journal of Guidance Control and Dynamics, 16(2),
661
    #   320-329, 2012. http://doi.org/10.2514/3.21006
662
    #
663

664
    # Set up the full problem
665
    # Create matrix of (shifted) inputs
666
    UUT = np.zeros((p*m,(l)))
9✔
667
    for i in range(m):
9✔
668
        # Shift previous column down and keep zeros at the top
669
        UUT[i*p:(i+1)*p,i:] = Umat[:,:l-i]
9✔
670

671
    # Truncate first t=0 or t=m time steps, transpose the problem for lsq
672
    YY = Ymat[:,t:].T
9✔
673
    UU = UUT[:,t:].T
9✔
674
    
675
    # Solve for the Markov parameters from  YY = UU @ H.T
676
    HT, _, _, _ = np.linalg.lstsq(UU, YY, rcond=None)
9✔
677
    H = HT.T/dt # scaling
9✔
678

679
    H = H.reshape(q,m,p) # output, time*input -> output, time, input
9✔
680
    H = H.transpose(0,2,1) # output, input, time
9✔
681

682
    # for siso return a 1D array instead of a 3D array
683
    if q == 1 and p == 1:
9✔
684
        H = np.squeeze(H)
9✔
685

686
    # Return the first m Markov parameters
687
    return H if not transpose else np.transpose(H)
9✔
688

689
# Function aliases
690
hsvd = hankel_singular_values
9✔
691
balred = balanced_reduction
9✔
692
modred = model_reduction
9✔
693
minreal = minimal_realization
9✔
694
era = eigensys_realization
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc