|
1 | 1 | # -*- coding: utf-8 -*-
|
2 |
| -""" |
3 |
| -Created on Thu Dec 21 08:06:12 2023 |
| 2 | +"""sysnorm.py |
| 3 | +
|
| 4 | +Functions for computing system norms. |
| 5 | +
|
| 6 | +Routine in this module: |
| 7 | +
|
| 8 | +norm |
4 | 9 |
|
5 |
| -@author: hsan |
| 10 | +Created on Thu Dec 21 08:06:12 2023 |
| 11 | +Author: Henrik Sandberg |
6 | 12 | """
|
7 | 13 |
|
8 | 14 | import numpy as np
|
9 | 15 | import numpy.linalg as la
|
10 | 16 |
|
11 | 17 | import control as ct
|
12 | 18 |
|
| 19 | +__all__ = ['norm'] |
| 20 | +
8000
td> |
13 | 21 | #------------------------------------------------------------------------------
|
14 | 22 |
|
15 |
| -def norm(system, p=2, tol=1e-6): |
16 |
| - """Computes H_2 (p=2) or L_infinity (p="inf", tolerance tol) norm of system.""" |
| 23 | +def norm(system, p=2, tol=1e-6, print_warning=True): |
| 24 | + """Computes norm of system. |
| 25 | + |
| 26 | + Parameters |
| 27 | + ---------- |
| 28 | + system : LTI (:class:`StateSpace` or :class:`TransferFunction`) |
| 29 | + System in continuous or discrete time for which the norm should be computed. |
| 30 | + p : int or str |
| 31 | + Type of norm to be computed. p=2 gives the H_2 norm, and p='inf' gives the L_infinity norm. |
| 32 | + tol : float |
| 33 | + Relative tolerance for accuracy of L_infinity norm computation. Ignored |
| 34 | + unless p='inf'. |
| 35 | + print_warning : bool |
| 36 | + Print warning message in case norm value may be uncertain. |
| 37 | + |
| 38 | + Returns |
| 39 | + ------- |
| 40 | + norm_value : float or NoneType |
| 41 | + Norm value of system (float) or None if computation could not be completed.
| 42 | + |
| 43 | + Notes |
| 44 | + ----- |
| 45 | + Does not yet compute the L_infinity norm for discrete time systems with pole(s) in z=0. |
| 46 | + |
| 47 | + Examples |
| 48 | + -------- |
| 49 | + >>> Gc = ct.tf([1], [1, 2, 1]) |
| 50 | + >>> ct.norm(Gc,2) |
| 51 | + 0.5000000000000001 |
| 52 | + >>> ct.norm(Gc,'inf',tol=1e-10) |
| 53 | + 1.0000000000582077 |
| 54 | + """ |
17 | 55 | G = ct.ss(system)
|
18 | 56 | A = G.A
|
19 | 57 | B = G.B
|
20 | 58 | C = G.C
|
21 | 59 | D = G.D
|
22 | 60 |
|
23 |
| - if p == 2: # H_2-norm |
| 61 | + # |
| 62 | + # H_2-norm computation |
| 63 | + # |
| 64 | + if p == 2: |
| 65 | + # Continuous time case |
24 | 66 | if G.isctime():
|
25 |
| - if (D != 0).any() or any(G.poles().real >= 0): |
| 67 | + poles_real_part = G.poles().real |
| 68 | + if any(np.isclose(poles_real_part, 0.0)): |
| 69 | + if print_warning: |
| 70 | + print("Warning: Poles close to, or on, the imaginary axis. Norm value may be uncertain.") |
| 71 | + return float('inf') |
| 72 | + elif (D != 0).any() or any(poles_real_part > 0.0): # System unstable or has direct feedthrough? |
26 | 73 | return float('inf')
|
27 | 74 | else:
|
28 |
| - P = ct.lyap(A, B@B.T) |
29 |
| - return np.sqrt(np.trace(C@P@C.T)) |
| 75 | + try: |
| 76 | + P = ct.lyap(A, B@B.T) |
| 77 | + except Exception as e: |
| 78 | + print(f"An error occurred solving the continuous time Lyapunov equation: {e}") |
| 79 | + return None |
| 80 | + |
| 81 | + # System is stable to reach this point, and P should be positive semi-definite. |
| 82 | + # Test next is a precaution in case the Lyapunov equation is ill conditioned. |
| 83 | + if any(la.eigvals(P) < 0.0): |
| 84 | + if print_warning: |
| 85 | + print("Warning: There appears to be poles close to the imaginary axis. Norm value may be uncertain.") |
| 86 | + return float('inf') |
| 87 | + else: |
| 88 | + norm_value = np.sqrt(np.trace(C@P@C.T)) # Argument in sqrt should be non-negative |
| 89 | + if np.isnan(norm_value): |
| 90 | + print("Unknown error. Norm computation resulted in NaN.") |
| 91 | + return None |
| 92 | + else: |
| 93 | + return norm_value |
| 94 | + |
| 95 | + # Discrete time case |
30 | 96 | elif G.isdtime():
|
31 |
| - if any(abs(G.poles()) >= 1): |
| 97 | + poles_abs = abs(G.poles()) |
| 98 | + if any(np.isclose(poles_abs, 1.0)): |
| 99 | + if print_warning: |
| 100 | + print("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain.") |
| 101 | + return float('inf') |
| 102 | + elif any(poles_abs > 1.0): # System unstable? |
32 | 103 | return float('inf')
|
33 | 104 | else:
|
34 |
| - P = ct.dlyap(A, B@B.T) |
35 |
| - return np.sqrt(np.trace(C@P@C.T + D@D.T)) |
36 |
| - |
37 |
| - elif p == "inf": # L_infinity-norm |
38 |
| - def Hamilton_matrix(gamma): |
39 |
| - """Constructs Hamiltonian matrix.""" |
| 105 | + try: |
| 106 | + P = ct.dlyap(A, B@B.T) |
| 107 | + except Exception as e: |
| 108 | + print(f"An error occurred solving the discrete time Lyapunov equation: {e}") |
| 109 | + return None |
| 110 | + |
| 111 | + # System is stable to reach this point, and P should be positive semi-definite. |
| 112 | + # Test next is a precaution in case the Lyapunov equation is ill conditioned. |
| 113 | + if any(la.eigvals(P) < 0.0): |
| 114 | + if print_warning: |
| 115 | + print("Warning: There appears to be poles close to the complex unit circle. Norm value may be uncertain.") |
| 116 | + return float('inf') |
| 117 | + else: |
| 118 | + norm_value = np.sqrt(np.trace(C@P@C.T + D@D.T)) # Argument in sqrt should be non-negative |
| 119 | + if np.isnan(norm_value): |
| 120 | + print("Unknown error. Norm computation resulted in NaN.") |
| 121 | + return None |
| 122 | + else: |
| 123 | + return norm_value |
| 124 | + # |
| 125 | + # L_infinity-norm computation |
| 126 | + # |
| 127 | + elif p == "inf": |
| 128 | + def _Hamilton_matrix(gamma): |
| 129 | + """Constructs Hamiltonian matrix. For internal use.""" |
40 | 130 | R = Ip*gamma**2 - D.T@D
|
41 | 131 | invR = la.inv(R)
|
42 | 132 | return np.block([[A+B@invR@D.T@C, B@invR@B.T], [-C.T@(Ip+D@invR@D.T)@C, -(A+B@invR@D.T@C).T]])
|
43 | 133 |
|
44 |
| - if G.isdtime(): # Bilinear transformation to s-plane |
| 134 | + # Discrete time case |
| 135 | + # Use inverse bilinear transformation of discrete time system to s-plane if no poles on |z|=1 or z=0. |
| 136 | + # Allows us to use test for continuous time systems next. |
| 137 | + if G.isdtime(): |
45 | 138 | Ad = A
|
46 | 139 | Bd = B
|
47 | 140 | Cd = C
|
48 | 141 | Dd = D
|
| 142 | + if any(np.isclose(abs(la.eigvals(Ad)), 1.0)): |
| 143 | + if print_warning: |
| 144 | + print("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain.") |
| 145 | + return float('inf') |
| 146 | + elif any(np.isclose(la.eigvals(Ad), 0.0)): |
| 147 | + print("L_infinity norm computation for discrete time system with pole(s) at z=0 currently not supported.") |
| 148 | + return None |
| 149 | + |
| 150 | + # Inverse bilinear transformation |
49 | 151 | In = np.eye(len(Ad))
|
50 | 152 | Adinv = la.inv(Ad+In)
|
51 | 153 | A = 2*(Ad-In)@Adinv
|
52 | 154 | B = 2*Adinv@Bd
|
53 | 155 | C = 2*Cd@Adinv
|
54 | 156 | D = Dd - Cd@Adinv@Bd
|
55 |
| - |
| 157 | + |
| 158 | + # Continus time case |
56 | 159 | if any(np.isclose(la.eigvals(A).real, 0.0)):
|
| 160 | + if print_warning: |
| 161 | + print("Warning: Poles close to, or on, imaginary axis. Norm value may be uncertain.") |
57 | 162 | return float('inf')
|
58 | 163 |
|
59 |
| - gaml = la.norm(D,ord=2) # Lower bound |
60 |
| - gamu = max(1.0, 2.0*gaml) # Candidate upper bound |
| 164 | + gaml = la.norm(D,ord=2) # Lower bound |
| 165 | + gamu = max(1.0, 2.0*gaml) # Candidate upper bound |
61 | 166 | Ip = np.eye(len(D))
|
62 | 167 |
|
63 |
| - while any(np.isclose(la.eigvals(Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound |
| 168 | + while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find actual upper bound |
64 | 169 | gamu *= 2.0
|
65 | 170 |
|
66 | 171 | while (gamu-gaml)/gamu > tol:
|
67 | 172 | gam = (gamu+gaml)/2.0
|
68 |
| - if any(np.isclose(la.eigvals(Hamilton_matrix(gam)).real, 0.0)): |
| 173 | + if any(np.isclose(la.eigvals(_Hamilton_matrix(gam)).real, 0.0)): |
69 | 174 | gaml = gam
|
70 | 175 | else:
|
71 | 176 | gamu = gam
|
72 | 177 | return gam
|
73 | 178 | else:
|
74 |
| - # Norm computation only supported for p=2 and p='inf' |
| 179 | + print(f"Norm computation for p={p} currently not supported.") |
75 | 180 | return None
|
0 commit comments