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

python-control / python-control / 10370763703

13 Aug 2024 01:32PM UTC coverage: 94.693% (-0.001%) from 94.694%
10370763703

push

github

web-flow
Merge pull request #1038 from murrayrm/doc-comment_fixes-11May2024

Documentation updates and docstring unit tests

9136 of 9648 relevant lines covered (94.69%)

8.27 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
    """Model reduction by state elimination.
113
    
114
    Model reduction of `sys` by eliminating the states in `ELIM` using a given
115
    method.
116

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

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

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

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

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

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

149
    """
150

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

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

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

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

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

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

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

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

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

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

221

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

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

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

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

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

264
    ValueError
265
        if there are more unstable modes than any value in orders
266

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

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

289
    # Check for ss system object, need a utility for this?
290

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

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

307
    rsys = []                   # empty list for reduced systems
5✔
308

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

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

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

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

346

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

355
    Parameters
356
    ----------
357
    sys : StateSpace or TransferFunction
358
        Original system.
359
    tol : real
360
        Tolerance.
361
    verbose : bool
362
        Print results if True.
363

364
    Returns
365
    -------
366
    rsys : StateSpace or TransferFunction
367
        Cleaned model.
368

369
    """
370
    sysr = sys.minreal(tol)
5✔
371
    if verbose:
5✔
372
        print("{nstates} states have been removed from the model".format(
5✔
373
                nstates=len(sys.poles()) - len(sysr.poles())))
374
    return sysr
5✔
375

376

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

391

392
def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False):
9✔
393
    r"""eigensys_realization(YY, r)
394

395
    Calculate ERA model of order `r` based on impulse-response data `YY`.
396

397
    This function computes a discrete time system
398

399
    .. math::
400

401
        x[k+1] &= A x[k] + B u[k] \\\\
402
        y[k] &= C x[k] + D u[k]
403

404
    for a given impulse-response data (see [1]_).
405

406
    The function can be called with 2 arguments:
407

408
    * ``sysd, S = eigensys_realization(data, r)``
409
    * ``sysd, S = eigensys_realization(YY, r)``
410

411
    where `data` is a `TimeResponseData` object, `YY` is a 1D or 3D
412
    array, and r is an integer.
413

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

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

445
    References
446
    ----------
447
    .. [1] Samet Oymak and Necmiye Ozay, Non-asymptotic Identification of
448
       LTI Systems from a Single Trajectory.
449
       https://arxiv.org/abs/1806.05722
450

451
    Examples
452
    --------
453
    >>> T = np.linspace(0, 10, 100)
454
    >>> _, YY = ct.impulse_response(ct.tf([1], [1, 0.5], True), T)
455
    >>> sysd, _ = ct.eigensys_realization(YY, r=1)
456

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

472
    if m is None:
9✔
473
        m = 2*r
9✔
474
    if n is None:
9✔
475
        n = 2*r
9✔
476

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

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

498
    return StateSpace(Ar,Br,Cr,Dr,dt), S
9✔
499

500

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

506
    This function computes the Markov parameters for a discrete time
507
    system
508

509
    .. math::
510

511
        x[k+1] &= A x[k] + B u[k] \\\\
512
        y[k] &= C x[k] + D u[k]
513

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

519
    The function can be called with either 1, 2 or 3 arguments:
520

521
    * ``H = markov(data)``
522
    * ``H = markov(data, m)``
523
    * ``H = markov(Y, U)``
524
    * ``H = markov(Y, U, m)``
525

526
    where `data` is a `TimeResponseData` object, `YY` is a 1D or 3D
527
    array, and r is an integer.
528

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

557
    Returns
558
    -------
559
    H : ndarray
560
        First m Markov parameters, [D CB CAB ...].
561

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

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

576
    """
577

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

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

619
    # If number of desired parameters was not given, set to size of input data
620
    if m is None:
9✔
621
        m = l
9✔
622

623
    t = 0
9✔
624
    if truncate:
9✔
625
        t = m
9✔
626

627
    q = Ymat.shape[0] # number of outputs
9✔
628
    p = Umat.shape[0] # number of inputs
9✔
629

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

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

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

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

683
    H = H.reshape(q,m,p) # output, time*input -> output, time, input
9✔
684
    H = H.transpose(0,2,1) # output, input, time
9✔
685

686
    # for siso return a 1D array instead of a 3D array
687
    if q == 1 and p == 1:
9✔
688
        H = np.squeeze(H)
9✔
689

690
    # Return the first m Markov parameters
691
    return H if not transpose else np.transpose(H)
9✔
692

693
# Function aliases
694
hsvd = hankel_singular_values
9✔
695
balred = balanced_reduction
9✔
696
modred = model_reduction
9✔
697
minreal = minimal_realization
9✔
698
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