From 6a0e136ed3dcb33a8399ddda67d774a5971780d6 Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Sun, 7 Jan 2024 21:53:32 +0100 Subject: [PATCH 01/22] New function for LTI system norm computation --- control/__init__.py | 1 + control/sysnorm.py | 75 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 control/sysnorm.py diff --git a/control/__init__.py b/control/__init__.py index 120d16325..5a9e05e95 100644 --- a/control/__init__.py +++ b/control/__init__.py @@ -101,6 +101,7 @@ from .config import * from .sisotool import * from .passivity import * +from .sysnorm import * # Exceptions from .exception import * diff --git a/control/sysnorm.py b/control/sysnorm.py new file mode 100644 index 000000000..5caae0918 --- /dev/null +++ b/control/sysnorm.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Dec 21 08:06:12 2023 + +@author: hsan +""" + +import numpy as np +import numpy.linalg as la + +import control as ct + +#------------------------------------------------------------------------------ + +def norm(system, p=2, tol=1e-6): + """Computes H_2 (p=2) or L_infinity (p="inf", tolerance tol) norm of system.""" + G = ct.ss(system) + A = G.A + B = G.B + C = G.C + D = G.D + + if p == 2: # H_2-norm + if G.isctime(): + if (D != 0).any() or any(G.poles().real >= 0): + return float('inf') + else: + P = ct.lyap(A, B@B.T) + return np.sqrt(np.trace(C@P@C.T)) + elif G.isdtime(): + if any(abs(G.poles()) >= 1): + return float('inf') + else: + P = ct.dlyap(A, B@B.T) + return np.sqrt(np.trace(C@P@C.T + D@D.T)) + + elif p == "inf": # L_infinity-norm + def Hamilton_matrix(gamma): + """Constructs Hamiltonian matrix.""" + R = Ip*gamma**2 - D.T@D + invR = la.inv(R) + 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]]) + + if G.isdtime(): # Bilinear transformation to s-plane + Ad = A + Bd = B + Cd = C + Dd = D + In = np.eye(len(Ad)) + Adinv = la.inv(Ad+In) + A = 2*(Ad-In)@Adinv + B = 2*Adinv@Bd + C = 2*Cd@Adinv + D = Dd - Cd@Adinv@Bd + + if any(np.isclose(la.eigvals(A).real, 0.0)): + return float('inf') + + gaml = la.norm(D,ord=2) # Lower bound + gamu = max(1.0, 2.0*gaml) # Candidate upper bound + Ip = np.eye(len(D)) + + while any(np.isclose(la.eigvals(Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound + gamu *= 2.0 + + while (gamu-gaml)/gamu > tol: + gam = (gamu+gaml)/2.0 + if any(np.isclose(la.eigvals(Hamilton_matrix(gam)).real, 0.0)): + gaml = gam + else: + gamu = gam + return gam + else: + # Norm computation only supported for p=2 and p='inf' + return None From a0fbc80ded43bcf14540331c0931c1f5da44c0dc Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Mon, 8 Jan 2024 13:51:33 +0100 Subject: [PATCH 02/22] * Updated documentation of function norm * Added control/tests/sysnorm_test.py --- control/sysnorm.py | 57 ++++++++++++++++++++++++++----- control/tests/sysnorm_test.py | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 control/tests/sysnorm_test.py diff --git a/control/sysnorm.py b/control/sysnorm.py index 5caae0918..074055254 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -1,8 +1,14 @@ # -*- coding: utf-8 -*- -""" -Created on Thu Dec 21 08:06:12 2023 +"""sysnorm.py + +Functions for computing system norms. -@author: hsan +Routines in this module: + +norm() + +Created on Thu Dec 21 08:06:12 2023 +Author: Henrik Sandberg """ import numpy as np @@ -13,7 +19,35 @@ #------------------------------------------------------------------------------ def norm(system, p=2, tol=1e-6): - """Computes H_2 (p=2) or L_infinity (p="inf", tolerance tol) norm of system.""" + """Computes norm of system. + + Parameters + ---------- + system : LTI (:class:`StateSpace` or :class:`TransferFunction`) + System in continuous or discrete time for which the norm should be computed. + p : int or str + Type of norm to be computed. p=2 gives the H_2 norm, and p='inf' gives the L_infinity norm. + tol : float + Relative tolerance for accuracy of L_infinity norm computation. Ignored + unless p='inf'. + + Returns + ------- + norm : float + Norm of system + + Notes + ----- + Does not yet compute the L_infinity norm for discrete time systems with pole(s) in z=0. + + Examples + -------- + >>> Gc = ct.tf([1], [1, 2, 1]) + >>> ct.norm(Gc,2) + 0.5000000000000001 + >>> ct.norm(Gc,'inf',tol=1e-10) + 1.0000000000582077 + """ G = ct.ss(system) A = G.A B = G.B @@ -35,17 +69,22 @@ def norm(system, p=2, tol=1e-6): return np.sqrt(np.trace(C@P@C.T + D@D.T)) elif p == "inf": # L_infinity-norm - def Hamilton_matrix(gamma): + def _Hamilton_matrix(gamma): """Constructs Hamiltonian matrix.""" R = Ip*gamma**2 - D.T@D invR = la.inv(R) 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]]) - if G.isdtime(): # Bilinear transformation to s-plane + if G.isdtime(): # Bilinear transformation of discrete time system to s-plane if no poles at |z|=1 or z=0 Ad = A Bd = B Cd = C Dd = D + if any(np.isclose(abs(la.eigvals(Ad)), 1.0)): + return float('inf') + elif any(np.isclose(la.eigvals(Ad), 0.0)): + print("L_infinity norm computation for discrete time system with pole(s) at z = 0 currently not supported.") + return None In = np.eye(len(Ad)) Adinv = la.inv(Ad+In) A = 2*(Ad-In)@Adinv @@ -60,16 +99,16 @@ def Hamilton_matrix(gamma): gamu = max(1.0, 2.0*gaml) # Candidate upper bound Ip = np.eye(len(D)) - while any(np.isclose(la.eigvals(Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound + while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound gamu *= 2.0 while (gamu-gaml)/gamu > tol: gam = (gamu+gaml)/2.0 - if any(np.isclose(la.eigvals(Hamilton_matrix(gam)).real, 0.0)): + if any(np.isclose(la.eigvals(_Hamilton_matrix(gam)).real, 0.0)): gaml = gam else: gamu = gam return gam else: - # Norm computation only supported for p=2 and p='inf' + print("Norm computation for p =", p, "currently not supported.") return None diff --git a/control/tests/sysnorm_test.py b/control/tests/sysnorm_test.py new file mode 100644 index 000000000..915e64622 --- /dev/null +++ b/control/tests/sysnorm_test.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +""" +Tests for sysnorm module. + +Created on Mon Jan 8 11:31:46 2024 +Author: Henrik Sandberg +""" + +import control as ct +import numpy as np + +def test_norm_1st_order_stable_system(): + """First-order stable continuous-time system""" + s = ct.tf('s') + G1 = 1/(s+1) + assert np.allclose(ct.norm(G1, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G1, p=2), 0.707106781186547, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + + Gd1 = ct.sample_system(G1, 0.1) + assert np.allclose(ct.norm(Gd1, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd1, p=2), 0.223513699524858, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + + +def test_norm_1st_order_unstable_system(): + """First-order unstable continuous-time system""" + s = ct.tf('s') + G2 = 1/(1-s) + assert np.allclose(ct.norm(G2, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert ct.norm(G2, p=2) == float('inf') # Comparison to norm computed in MATLAB + + Gd2 = ct.sample_system(G2, 0.1) + assert np.allclose(ct.norm(Gd2, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert ct.norm(Gd2, p=2) == float('inf') # Comparison to norm computed in MATLAB + +def test_norm_2nd_order_system_imag_poles(): + """Second-order continuous-time system with poles on imaginary axis""" + s = ct.tf('s') + G3 = 1/(s**2+1) + assert ct.norm(G3, p='inf') == float('inf') # Comparison to norm computed in MATLAB + assert ct.norm(G3, p=2) == float('inf') # Comparison to norm computed in MATLAB + + Gd3 = ct.sample_system(G3, 0.1) + assert ct.norm(Gd3, p='inf') == float('inf') # Comparison to norm computed in MATLAB + assert ct.norm(Gd3, p=2) == float('inf') # Comparison to norm computed in MATLAB + +def test_norm_3rd_order_mimo_system(): + """Third-order stable MIMO continuous-time system""" + A = np.array([[-1.017041847539126, -0.224182952826418, 0.042538079149249], + [-0.310374015319095, -0.516461581407780, -0.119195790221750], + [-1.452723568727942, 1.7995860837102088, -1.491935830615152]]) + B = np.array([[0.312858596637428, -0.164879019209038], + [-0.864879917324456, 0.627707287528727], + [-0.030051296196269, 1.093265669039484]]) + C = np.array([[1.109273297614398, 0.077359091130425, -1.113500741486764], + [-0.863652821988714, -1.214117043615409, -0.006849328103348]]) + D = np.zeros((2,2)) + G4 = ct.ss(A,B,C,D) # Random system generated in MATLAB + assert np.allclose(ct.norm(G4, p='inf', tol=1e-9), 4.276759162964244, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G4, p=2), 2.237461821810309, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + + Gd4 = ct.sample_system(G4, 0.1) + assert np.allclose(ct.norm(Gd4, p='inf', tol=1e-9), 4.276759162964228, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd4, p=2), 0.707434962289554, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB From 457c62380554ef2fd6b2779c369deb2b18b5becb Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Tue, 9 Jan 2024 16:50:45 +0100 Subject: [PATCH 03/22] Update sysnorm.py * Added additional tests and warning messages for systems with poles close to stability boundary * Added __all__ * Added more comments --- control/sysnorm.py | 110 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 22 deletions(-) diff --git a/control/sysnorm.py b/control/sysnorm.py index 074055254..2065f8721 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -3,9 +3,9 @@ Functions for computing system norms. -Routines in this module: +Routine in this module: -norm() +norm Created on Thu Dec 21 08:06:12 2023 Author: Henrik Sandberg @@ -16,9 +16,11 @@ import control as ct +__all__ = ['norm'] + #------------------------------------------------------------------------------ -def norm(system, p=2, tol=1e-6): +def norm(system, p=2, tol=1e-6, print_warning=True): """Computes norm of system. Parameters @@ -30,11 +32,13 @@ def norm(system, p=2, tol=1e-6): tol : float Relative tolerance for accuracy of L_infinity norm computation. Ignored unless p='inf'. + print_warning : bool + Print warning message in case norm value may be uncertain. Returns ------- - norm : float - Norm of system + norm_value : float or NoneType + Norm value of system (float) or None if computation could not be completed. Notes ----- @@ -54,52 +58,114 @@ def norm(system, p=2, tol=1e-6): C = G.C D = G.D - if p == 2: # H_2-norm + # + # H_2-norm computation + # + if p == 2: + # Continuous time case if G.isctime(): - if (D != 0).any() or any(G.poles().real >= 0): + poles_real_part = G.poles().real + if any(np.isclose(poles_real_part, 0.0)): + if print_warning: + print("Warning: Poles close to, or on, the imaginary axis. Norm value may be uncertain.") + return float('inf') + elif (D != 0).any() or any(poles_real_part > 0.0): # System unstable or has direct feedthrough? return float('inf') else: - P = ct.lyap(A, B@B.T) - return np.sqrt(np.trace(C@P@C.T)) + try: + P = ct.lyap(A, B@B.T) + except Exception as e: + print(f"An error occurred solving the continuous time Lyapunov equation: {e}") + return None + + # System is stable to reach this point, and P should be positive semi-definite. + # Test next is a precaution in case the Lyapunov equation is ill conditioned. + if any(la.eigvals(P) < 0.0): + if print_warning: + print("Warning: There appears to be poles close to the imaginary axis. Norm value may be uncertain.") + return float('inf') + else: + norm_value = np.sqrt(np.trace(C@P@C.T)) # Argument in sqrt should be non-negative + if np.isnan(norm_value): + print("Unknown error. Norm computation resulted in NaN.") + return None + else: + return norm_value + + # Discrete time case elif G.isdtime(): - if any(abs(G.poles()) >= 1): + poles_abs = abs(G.poles()) + if any(np.isclose(poles_abs, 1.0)): + if print_warning: + print("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain.") + return float('inf') + elif any(poles_abs > 1.0): # System unstable? return float('inf') else: - P = ct.dlyap(A, B@B.T) - return np.sqrt(np.trace(C@P@C.T + D@D.T)) - - elif p == "inf": # L_infinity-norm + try: + P = ct.dlyap(A, B@B.T) + except Exception as e: + print(f"An error occurred solving the discrete time Lyapunov equation: {e}") + return None + + # System is stable to reach this point, and P should be positive semi-definite. + # Test next is a precaution in case the Lyapunov equation is ill conditioned. + if any(la.eigvals(P) < 0.0): + if print_warning: + print("Warning: There appears to be poles close to the complex unit circle. Norm value may be uncertain.") + return float('inf') + else: + norm_value = np.sqrt(np.trace(C@P@C.T + D@D.T)) # Argument in sqrt should be non-negative + if np.isnan(norm_value): + print("Unknown error. Norm computation resulted in NaN.") + return None + else: + return norm_value + # + # L_infinity-norm computation + # + elif p == "inf": def _Hamilton_matrix(gamma): - """Constructs Hamiltonian matrix.""" + """Constructs Hamiltonian matrix. For internal use.""" R = Ip*gamma**2 - D.T@D invR = la.inv(R) 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]]) - if G.isdtime(): # Bilinear transformation of discrete time system to s-plane if no poles at |z|=1 or z=0 + # Discrete time case + # Use inverse bilinear transformation of discrete time system to s-plane if no poles on |z|=1 or z=0. + # Allows us to use test for continuous time systems next. + if G.isdtime(): Ad = A Bd = B Cd = C Dd = D if any(np.isclose(abs(la.eigvals(Ad)), 1.0)): + if print_warning: + print("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain.") return float('inf') elif any(np.isclose(la.eigvals(Ad), 0.0)): - print("L_infinity norm computation for discrete time system with pole(s) at z = 0 currently not supported.") + print("L_infinity norm computation for discrete time system with pole(s) at z=0 currently not supported.") return None + + # Inverse bilinear transformation In = np.eye(len(Ad)) Adinv = la.inv(Ad+In) A = 2*(Ad-In)@Adinv B = 2*Adinv@Bd C = 2*Cd@Adinv D = Dd - Cd@Adinv@Bd - + + # Continus time case if any(np.isclose(la.eigvals(A).real, 0.0)): + if print_warning: + print("Warning: Poles close to, or on, imaginary axis. Norm value may be uncertain.") return float('inf') - gaml = la.norm(D,ord=2) # Lower bound - gamu = max(1.0, 2.0*gaml) # Candidate upper bound + gaml = la.norm(D,ord=2) # Lower bound + gamu = max(1.0, 2.0*gaml) # Candidate upper bound Ip = np.eye(len(D)) - while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound + while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find actual upper bound gamu *= 2.0 while (gamu-gaml)/gamu > tol: @@ -110,5 +176,5 @@ def _Hamilton_matrix(gamma): gamu = gam return gam else: - print("Norm computation for p =", p, "currently not supported.") + print(f"Norm computation for p={p} currently not supported.") return None From fd076c51173c8a1eb024dac8c94ac9946cbfdce5 Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Sun, 7 Jan 2024 21:53:32 +0100 Subject: [PATCH 04/22] New function for LTI system norm computation --- control/__init__.py | 1 + control/sysnorm.py | 75 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 control/sysnorm.py diff --git a/control/__init__.py b/control/__init__.py index 120d16325..5a9e05e95 100644 --- a/control/__init__.py +++ b/control/__init__.py @@ -101,6 +101,7 @@ from .config import * from .sisotool import * from .passivity import * +from .sysnorm import * # Exceptions from .exception import * diff --git a/control/sysnorm.py b/control/sysnorm.py new file mode 100644 index 000000000..5caae0918 --- /dev/null +++ b/control/sysnorm.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Dec 21 08:06:12 2023 + +@author: hsan +""" + +import numpy as np +import numpy.linalg as la + +import control as ct + +#------------------------------------------------------------------------------ + +def norm(system, p=2, tol=1e-6): + """Computes H_2 (p=2) or L_infinity (p="inf", tolerance tol) norm of system.""" + G = ct.ss(system) + A = G.A + B = G.B + C = G.C + D = G.D + + if p == 2: # H_2-norm + if G.isctime(): + if (D != 0).any() or any(G.poles().real >= 0): + return float('inf') + else: + P = ct.lyap(A, B@B.T) + return np.sqrt(np.trace(C@P@C.T)) + elif G.isdtime(): + if any(abs(G.poles()) >= 1): + return float('inf') + else: + P = ct.dlyap(A, B@B.T) + return np.sqrt(np.trace(C@P@C.T + D@D.T)) + + elif p == "inf": # L_infinity-norm + def Hamilton_matrix(gamma): + """Constructs Hamiltonian matrix.""" + R = Ip*gamma**2 - D.T@D + invR = la.inv(R) + 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]]) + + if G.isdtime(): # Bilinear transformation to s-plane + Ad = A + Bd = B + Cd = C + Dd = D + In = np.eye(len(Ad)) + Adinv = la.inv(Ad+In) + A = 2*(Ad-In)@Adinv + B = 2*Adinv@Bd + C = 2*Cd@Adinv + D = Dd - Cd@Adinv@Bd + + if any(np.isclose(la.eigvals(A).real, 0.0)): + return float('inf') + + gaml = la.norm(D,ord=2) # Lower bound + gamu = max(1.0, 2.0*gaml) # Candidate upper bound + Ip = np.eye(len(D)) + + while any(np.isclose(la.eigvals(Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound + gamu *= 2.0 + + while (gamu-gaml)/gamu > tol: + gam = (gamu+gaml)/2.0 + if any(np.isclose(la.eigvals(Hamilton_matrix(gam)).real, 0.0)): + gaml = gam + else: + gamu = gam + return gam + else: + # Norm computation only supported for p=2 and p='inf' + return None From 0fe2c573797857ffbb6b9ece660f030c39f394e3 Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Mon, 8 Jan 2024 13:51:33 +0100 Subject: [PATCH 05/22] * Updated documentation of function norm * Added control/tests/sysnorm_test.py --- control/sysnorm.py | 57 ++++++++++++++++++++++++++----- control/tests/sysnorm_test.py | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 control/tests/sysnorm_test.py diff --git a/control/sysnorm.py b/control/sysnorm.py index 5caae0918..074055254 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -1,8 +1,14 @@ # -*- coding: utf-8 -*- -""" -Created on Thu Dec 21 08:06:12 2023 +"""sysnorm.py + +Functions for computing system norms. -@author: hsan +Routines in this module: + +norm() + +Created on Thu Dec 21 08:06:12 2023 +Author: Henrik Sandberg """ import numpy as np @@ -13,7 +19,35 @@ #------------------------------------------------------------------------------ def norm(system, p=2, tol=1e-6): - """Computes H_2 (p=2) or L_infinity (p="inf", tolerance tol) norm of system.""" + """Computes norm of system. + + Parameters + ---------- + system : LTI (:class:`StateSpace` or :class:`TransferFunction`) + System in continuous or discrete time for which the norm should be computed. + p : int or str + Type of norm to be computed. p=2 gives the H_2 norm, and p='inf' gives the L_infinity norm. + tol : float + Relative tolerance for accuracy of L_infinity norm computation. Ignored + unless p='inf'. + + Returns + ------- + norm : float + Norm of system + + Notes + ----- + Does not yet compute the L_infinity norm for discrete time systems with pole(s) in z=0. + + Examples + -------- + >>> Gc = ct.tf([1], [1, 2, 1]) + >>> ct.norm(Gc,2) + 0.5000000000000001 + >>> ct.norm(Gc,'inf',tol=1e-10) + 1.0000000000582077 + """ G = ct.ss(system) A = G.A B = G.B @@ -35,17 +69,22 @@ def norm(system, p=2, tol=1e-6): return np.sqrt(np.trace(C@P@C.T + D@D.T)) elif p == "inf": # L_infinity-norm - def Hamilton_matrix(gamma): + def _Hamilton_matrix(gamma): """Constructs Hamiltonian matrix.""" R = Ip*gamma**2 - D.T@D invR = la.inv(R) 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]]) - if G.isdtime(): # Bilinear transformation to s-plane + if G.isdtime(): # Bilinear transformation of discrete time system to s-plane if no poles at |z|=1 or z=0 Ad = A Bd = B Cd = C Dd = D + if any(np.isclose(abs(la.eigvals(Ad)), 1.0)): + return float('inf') + elif any(np.isclose(la.eigvals(Ad), 0.0)): + print("L_infinity norm computation for discrete time system with pole(s) at z = 0 currently not supported.") + return None In = np.eye(len(Ad)) Adinv = la.inv(Ad+In) A = 2*(Ad-In)@Adinv @@ -60,16 +99,16 @@ def Hamilton_matrix(gamma): gamu = max(1.0, 2.0*gaml) # Candidate upper bound Ip = np.eye(len(D)) - while any(np.isclose(la.eigvals(Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound + while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound gamu *= 2.0 while (gamu-gaml)/gamu > tol: gam = (gamu+gaml)/2.0 - if any(np.isclose(la.eigvals(Hamilton_matrix(gam)).real, 0.0)): + if any(np.isclose(la.eigvals(_Hamilton_matrix(gam)).real, 0.0)): gaml = gam else: gamu = gam return gam else: - # Norm computation only supported for p=2 and p='inf' + print("Norm computation for p =", p, "currently not supported.") return None diff --git a/control/tests/sysnorm_test.py b/control/tests/sysnorm_test.py new file mode 100644 index 000000000..915e64622 --- /dev/null +++ b/control/tests/sysnorm_test.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +""" +Tests for sysnorm module. + +Created on Mon Jan 8 11:31:46 2024 +Author: Henrik Sandberg +""" + +import control as ct +import numpy as np + +def test_norm_1st_order_stable_system(): + """First-order stable continuous-time system""" + s = ct.tf('s') + G1 = 1/(s+1) + assert np.allclose(ct.norm(G1, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G1, p=2), 0.707106781186547, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + + Gd1 = ct.sample_system(G1, 0.1) + assert np.allclose(ct.norm(Gd1, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd1, p=2), 0.223513699524858, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + + +def test_norm_1st_order_unstable_system(): + """First-order unstable continuous-time system""" + s = ct.tf('s') + G2 = 1/(1-s) + assert np.allclose(ct.norm(G2, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert ct.norm(G2, p=2) == float('inf') # Comparison to norm computed in MATLAB + + Gd2 = ct.sample_system(G2, 0.1) + assert np.allclose(ct.norm(Gd2, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert ct.norm(Gd2, p=2) == float('inf') # Comparison to norm computed in MATLAB + +def test_norm_2nd_order_system_imag_poles(): + """Second-order continuous-time system with poles on imaginary axis""" + s = ct.tf('s') + G3 = 1/(s**2+1) + assert ct.norm(G3, p='inf') == float('inf') # Comparison to norm computed in MATLAB + assert ct.norm(G3, p=2) == float('inf') # Comparison to norm computed in MATLAB + + Gd3 = ct.sample_system(G3, 0.1) + assert ct.norm(Gd3, p='inf') == float('inf') # Comparison to norm computed in MATLAB + assert ct.norm(Gd3, p=2) == float('inf') # Comparison to norm computed in MATLAB + +def test_norm_3rd_order_mimo_system(): + """Third-order stable MIMO continuous-time system""" + A = np.array([[-1.017041847539126, -0.224182952826418, 0.042538079149249], + [-0.310374015319095, -0.516461581407780, -0.119195790221750], + [-1.452723568727942, 1.7995860837102088, -1.491935830615152]]) + B = np.array([[0.312858596637428, -0.164879019209038], + [-0.864879917324456, 0.627707287528727], + [-0.030051296196269, 1.093265669039484]]) + C = np.array([[1.109273297614398, 0.077359091130425, -1.113500741486764], + [-0.863652821988714, -1.214117043615409, -0.006849328103348]]) + D = np.zeros((2,2)) + G4 = ct.ss(A,B,C,D) # Random system generated in MATLAB + assert np.allclose(ct.norm(G4, p='inf', tol=1e-9), 4.276759162964244, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G4, p=2), 2.237461821810309, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + + Gd4 = ct.sample_system(G4, 0.1) + assert np.allclose(ct.norm(Gd4, p='inf', tol=1e-9), 4.276759162964228, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd4, p=2), 0.707434962289554, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB From cdf4babd36648e61f6e4e8c9bfc2a952f43067a7 Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Tue, 9 Jan 2024 16:50:45 +0100 Subject: [PATCH 06/22] Update sysnorm.py * Added additional tests and warning messages for systems with poles close to stability boundary * Added __all__ * Added more comments --- control/sysnorm.py | 110 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 22 deletions(-) diff --git a/control/sysnorm.py b/control/sysnorm.py index 074055254..2065f8721 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -3,9 +3,9 @@ Functions for computing system norms. -Routines in this module: +Routine in this module: -norm() +norm Created on Thu Dec 21 08:06:12 2023 Author: Henrik Sandberg @@ -16,9 +16,11 @@ import control as ct +__all__ = ['norm'] + #------------------------------------------------------------------------------ -def norm(system, p=2, tol=1e-6): +def norm(system, p=2, tol=1e-6, print_warning=True): """Computes norm of system. Parameters @@ -30,11 +32,13 @@ def norm(system, p=2, tol=1e-6): tol : float Relative tolerance for accuracy of L_infinity norm computation. Ignored unless p='inf'. + print_warning : bool + Print warning message in case norm value may be uncertain. Returns ------- - norm : float - Norm of system + norm_value : float or NoneType + Norm value of system (float) or None if computation could not be completed. Notes ----- @@ -54,52 +58,114 @@ def norm(system, p=2, tol=1e-6): C = G.C D = G.D - if p == 2: # H_2-norm + # + # H_2-norm computation + # + if p == 2: + # Continuous time case if G.isctime(): - if (D != 0).any() or any(G.poles().real >= 0): + poles_real_part = G.poles().real + if any(np.isclose(poles_real_part, 0.0)): + if print_warning: + print("Warning: Poles close to, or on, the imaginary axis. Norm value may be uncertain.") + return float('inf') + elif (D != 0).any() or any(poles_real_part > 0.0): # System unstable or has direct feedthrough? return float('inf') else: - P = ct.lyap(A, B@B.T) - return np.sqrt(np.trace(C@P@C.T)) + try: + P = ct.lyap(A, B@B.T) + except Exception as e: + print(f"An error occurred solving the continuous time Lyapunov equation: {e}") + return None + + # System is stable to reach this point, and P should be positive semi-definite. + # Test next is a precaution in case the Lyapunov equation is ill conditioned. + if any(la.eigvals(P) < 0.0): + if print_warning: + print("Warning: There appears to be poles close to the imaginary axis. Norm value may be uncertain.") + return float('inf') + else: + norm_value = np.sqrt(np.trace(C@P@C.T)) # Argument in sqrt should be non-negative + if np.isnan(norm_value): + print("Unknown error. Norm computation resulted in NaN.") + return None + else: + return norm_value + + # Discrete time case elif G.isdtime(): - if any(abs(G.poles()) >= 1): + poles_abs = abs(G.poles()) + if any(np.isclose(poles_abs, 1.0)): + if print_warning: + print("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain.") + return float('inf') + elif any(poles_abs > 1.0): # System unstable? return float('inf') else: - P = ct.dlyap(A, B@B.T) - return np.sqrt(np.trace(C@P@C.T + D@D.T)) - - elif p == "inf": # L_infinity-norm + try: + P = ct.dlyap(A, B@B.T) + except Exception as e: + print(f"An error occurred solving the discrete time Lyapunov equation: {e}") + return None + + # System is stable to reach this point, and P should be positive semi-definite. + # Test next is a precaution in case the Lyapunov equation is ill conditioned. + if any(la.eigvals(P) < 0.0): + if print_warning: + print("Warning: There appears to be poles close to the complex unit circle. Norm value may be uncertain.") + return float('inf') + else: + norm_value = np.sqrt(np.trace(C@P@C.T + D@D.T)) # Argument in sqrt should be non-negative + if np.isnan(norm_value): + print("Unknown error. Norm computation resulted in NaN.") + return None + else: + return norm_value + # + # L_infinity-norm computation + # + elif p == "inf": def _Hamilton_matrix(gamma): - """Constructs Hamiltonian matrix.""" + """Constructs Hamiltonian matrix. For internal use.""" R = Ip*gamma**2 - D.T@D invR = la.inv(R) 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]]) - if G.isdtime(): # Bilinear transformation of discrete time system to s-plane if no poles at |z|=1 or z=0 + # Discrete time case + # Use inverse bilinear transformation of discrete time system to s-plane if no poles on |z|=1 or z=0. + # Allows us to use test for continuous time systems next. + if G.isdtime(): Ad = A Bd = B Cd = C Dd = D if any(np.isclose(abs(la.eigvals(Ad)), 1.0)): + if print_warning: + print("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain.") return float('inf') elif any(np.isclose(la.eigvals(Ad), 0.0)): - print("L_infinity norm computation for discrete time system with pole(s) at z = 0 currently not supported.") + print("L_infinity norm computation for discrete time system with pole(s) at z=0 currently not supported.") return None + + # Inverse bilinear transformation In = np.eye(len(Ad)) Adinv = la.inv(Ad+In) A = 2*(Ad-In)@Adinv B = 2*Adinv@Bd C = 2*Cd@Adinv D = Dd - Cd@Adinv@Bd - + + # Continus time case if any(np.isclose(la.eigvals(A).real, 0.0)): + if print_warning: + print("Warning: Poles close to, or on, imaginary axis. Norm value may be uncertain.") return float('inf') - gaml = la.norm(D,ord=2) # Lower bound - gamu = max(1.0, 2.0*gaml) # Candidate upper bound + gaml = la.norm(D,ord=2) # Lower bound + gamu = max(1.0, 2.0*gaml) # Candidate upper bound Ip = np.eye(len(D)) - while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound + while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find actual upper bound gamu *= 2.0 while (gamu-gaml)/gamu > tol: @@ -110,5 +176,5 @@ def _Hamilton_matrix(gamma): gamu = gam return gam else: - print("Norm computation for p =", p, "currently not supported.") + print(f"Norm computation for p={p} currently not supported.") return None From 34f95373f9d0b84964d25a8cfe1adb9f5463d1a3 Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Sun, 14 Jan 2024 13:03:21 +0100 Subject: [PATCH 07/22] Do not track changes in VS Code setup. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1b10a3585..4a6aa3cc0 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ TAGS # Files created by Spyder .spyproject/ +# Files created by or for VS Code (HS, 13 Jan, 2024) +.vscode/ + # Environments .env .venv From b419f12d4bb536004cc102b50057bab82fc1684a Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Sun, 21 Jan 2024 19:23:12 +0100 Subject: [PATCH 08/22] Lowered tolerances in tests. --- control/tests/sysnorm_test.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/control/tests/sysnorm_test.py b/control/tests/sysnorm_test.py index 915e64622..917e98d04 100644 --- a/control/tests/sysnorm_test.py +++ b/control/tests/sysnorm_test.py @@ -13,23 +13,23 @@ def test_norm_1st_order_stable_system(): """First-order stable continuous-time system""" s = ct.tf('s') G1 = 1/(s+1) - assert np.allclose(ct.norm(G1, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB - assert np.allclose(ct.norm(G1, p=2), 0.707106781186547, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G1, p='inf', tol=1e-9), 1.0) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G1, p=2), 0.707106781186547) # Comparison to norm computed in MATLAB Gd1 = ct.sample_system(G1, 0.1) - assert np.allclose(ct.norm(Gd1, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB - assert np.allclose(ct.norm(Gd1, p=2), 0.223513699524858, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd1, p='inf', tol=1e-9), 1.0) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd1, p=2), 0.223513699524858) # Comparison to norm computed in MATLAB def test_norm_1st_order_unstable_system(): """First-order unstable continuous-time system""" s = ct.tf('s') G2 = 1/(1-s) - assert np.allclose(ct.norm(G2, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G2, p='inf', tol=1e-9), 1.0) # Comparison to norm computed in MATLAB assert ct.norm(G2, p=2) == float('inf') # Comparison to norm computed in MATLAB Gd2 = ct.sample_system(G2, 0.1) - assert np.allclose(ct.norm(Gd2, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd2, p='inf', tol=1e-9), 1.0) # Comparison to norm computed in MATLAB assert ct.norm(Gd2, p=2) == float('inf') # Comparison to norm computed in MATLAB def test_norm_2nd_order_system_imag_poles(): @@ -55,9 +55,9 @@ def test_norm_3rd_order_mimo_system(): [-0.863652821988714, -1.214117043615409, -0.006849328103348]]) D = np.zeros((2,2)) G4 = ct.ss(A,B,C,D) # Random system generated in MATLAB - assert np.allclose(ct.norm(G4, p='inf', tol=1e-9), 4.276759162964244, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB - assert np.allclose(ct.norm(G4, p=2), 2.237461821810309, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G4, p='inf', tol=1e-9), 4.276759162964244) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G4, p=2), 2.237461821810309) # Comparison to norm computed in MATLAB Gd4 = ct.sample_system(G4, 0.1) - assert np.allclose(ct.norm(Gd4, p='inf', tol=1e-9), 4.276759162964228, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB - assert np.allclose(ct.norm(Gd4, p=2), 0.707434962289554, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd4, p='inf', tol=1e-9), 4.276759162964228) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd4, p=2), 0.707434962289554) # Comparison to norm computed in MATLAB From 9ecd5941ed8cf9844fa233ab193dbe018677c541 Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Sun, 21 Jan 2024 19:29:32 +0100 Subject: [PATCH 09/22] Added: * Use of warnings package. * Use routine statesp.linfnorm when slycot installed. * New routine internal _h2norm_slycot when slycot is installed. --- control/sysnorm.py | 285 +++++++++++++++++++++++++++++++-------------- 1 file changed, 195 insertions(+), 90 deletions(-) diff --git a/control/sysnorm.py b/control/sysnorm.py index 2065f8721..a25ef305f 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -12,7 +12,9 @@ """ import numpy as np +import scipy as sp import numpy.linalg as la +import warnings import control as ct @@ -20,7 +22,68 @@ #------------------------------------------------------------------------------ -def norm(system, p=2, tol=1e-6, print_warning=True): +def _h2norm_slycot(sys, print_warning=True): + """H2 norm of a linear system. For internal use. Requires Slycot. + + See also + -------- + slycot.ab13bd : the Slycot routine that does the calculation + https://github.com/python-control/Slycot/issues/199 : Post on issue with ab13bf + """ + + try: + from slycot import ab13bd + except ImportError: + ct.ControlSlycot("Can't find slycot module 'ab13bd'!") + + try: + from slycot.exceptions import SlycotArithmeticError + except ImportError: + raise ct.ControlSlycot("Can't find slycot class 'SlycotArithmeticError'!") + + A, B, C, D = ct.ssdata(ct.ss(sys)) + + n = A.shape[0] + m = B.shape[1] + p = C.shape[0] + + dico = 'C' if sys.isctime() else 'D' # Continuous or discrete time + jobn = 'H' # H2 (and not L2 norm) + + if n == 0: + # ab13bd does not accept empty A, B, C + if dico == 'C': + if any(D.flat != 0): + if print_warning: + warnings.warn("System has a direct feedthrough term!") + return float("inf") + else: + return 0.0 + elif dico == 'D': + return np.sqrt(D@D.T) + + try: + norm = ab13bd(dico, jobn, n, m, p, A, B, C, D) + except SlycotArithmeticError as e: + if e.info == 3: + if print_warning: + warnings.warn("System has pole(s) on the stability boundary!") + return float("inf") + elif e.info == 5: + if print_warning: + warnings.warn("System has a direct feedthrough term!") + return float("inf") + elif e.info == 6: + if print_warning: + warnings.warn("System is unstable!") + return float("inf") + else: + raise e + return norm + +#------------------------------------------------------------------------------ + +def norm(system, p=2, tol=1e-10, print_warning=True, use_slycot=True): """Computes norm of system. Parameters @@ -28,12 +91,14 @@ def norm(system, p=2, tol=1e-6, print_warning=True): system : LTI (:class:`StateSpace` or :class:`TransferFunction`) System in continuous or discrete time for which the norm should be computed. p : int or str - Type of norm to be computed. p=2 gives the H_2 norm, and p='inf' gives the L_infinity norm. + Type of norm to be computed. p=2 gives the H2 norm, and p='inf' gives the L-infinity norm. tol : float - Relative tolerance for accuracy of L_infinity norm computation. Ignored + Relative tolerance for accuracy of L-infinity norm computation. Ignored unless p='inf'. print_warning : bool Print warning message in case norm value may be uncertain. + use_slycot : bool + Use Slycot routines if available. Returns ------- @@ -42,7 +107,7 @@ def norm(system, p=2, tol=1e-6, print_warning=True): Notes ----- - Does not yet compute the L_infinity norm for discrete time systems with pole(s) in z=0. + Does not yet compute the L-infinity norm for discrete time systems with pole(s) in z=0 unless Slycot is used. Examples -------- @@ -58,123 +123,163 @@ def norm(system, p=2, tol=1e-6, print_warning=True): C = G.C D = G.D - # - # H_2-norm computation - # + # ------------------- + # H2 norm computation + # ------------------- if p == 2: + # -------------------- # Continuous time case + # -------------------- if G.isctime(): + + # Check for cases with infinite norm poles_real_part = G.poles().real - if any(np.isclose(poles_real_part, 0.0)): + if any(np.isclose(poles_real_part, 0.0)): # Poles on imaginary axis if print_warning: - print("Warning: Poles close to, or on, the imaginary axis. Norm value may be uncertain.") + warnings.warn("Poles close to, or on, the imaginary axis. Norm value may be uncertain.") return float('inf') - elif (D != 0).any() or any(poles_real_part > 0.0): # System unstable or has direct feedthrough? + elif any(poles_real_part > 0.0): # System unstable + if print_warning: + warnings.warn("System is unstable!") return float('inf') - else: - try: - P = ct.lyap(A, B@B.T) - except Exception as e: - print(f"An error occurred solving the continuous time Lyapunov equation: {e}") - return None + elif any(D.flat != 0): # System has direct feedthrough + if print_warning: + warnings.warn("System has a direct feedthrough term!") + return float('inf') + + else: + # Use slycot, if available, to compute (finite) norm + if ct.slycot_check() and use_slycot: + return _h2norm_slycot(G, print_warning) - # System is stable to reach this point, and P should be positive semi-definite. - # Test next is a precaution in case the Lyapunov equation is ill conditioned. - if any(la.eigvals(P) < 0.0): - if print_warning: - print("Warning: There appears to be poles close to the imaginary axis. Norm value may be uncertain.") - return float('inf') + # Else use scipy else: - norm_value = np.sqrt(np.trace(C@P@C.T)) # Argument in sqrt should be non-negative - if np.isnan(norm_value): - print("Unknown error. Norm computation resulted in NaN.") - return None + P = ct.lyap(A, B@B.T) # Solve for controllability Gramian + + # System is stable to reach this point, and P should be positive semi-definite. + # Test next is a precaution in case the Lyapunov equation is ill conditioned. + if any(la.eigvals(P).real < 0.0): + if print_warning: + warnings.warn("There appears to be poles close to the imaginary axis. Norm value may be uncertain.") + return float('inf') else: - return norm_value + norm_value = np.sqrt(np.trace(C@P@C.T)) # Argument in sqrt should be non-negative + if np.isnan(norm_value): + raise ct.ControlArgument("Norm computation resulted in NaN.") + else: + return norm_value + # ------------------ # Discrete time case + # ------------------ elif G.isdtime(): + + # Check for cases with infinite norm poles_abs = abs(G.poles()) - if any(np.isclose(poles_abs, 1.0)): + if any(np.isclose(poles_abs, 1.0)): # Poles on imaginary axis if print_warning: - print("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain.") + warnings.warn("Poles close to, or on, the complex unit circle. Norm value may be uncertain.") return float('inf') - elif any(poles_abs > 1.0): # System unstable? + elif any(poles_abs > 1.0): # System unstable + if print_warning: + warnings.warn("System is unstable!") return float('inf') + else: - try: + # Use slycot, if available, to compute (finite) norm + if ct.slycot_check() and use_slycot: + return _h2norm_slycot(G, print_warning) + + # Else use scipy + else: P = ct.dlyap(A, B@B.T) - except Exception as e: - print(f"An error occurred solving the discrete time Lyapunov equation: {e}") - return None # System is stable to reach this point, and P should be positive semi-definite. # Test next is a precaution in case the Lyapunov equation is ill conditioned. - if any(la.eigvals(P) < 0.0): + if any(la.eigvals(P).real < 0.0): if print_warning: - print("Warning: There appears to be poles close to the complex unit circle. Norm value may be uncertain.") + warnings.warn("Warning: There appears to be poles close to the complex unit circle. Norm value may be uncertain.") return float('inf') else: norm_value = np.sqrt(np.trace(C@P@C.T + D@D.T)) # Argument in sqrt should be non-negative if np.isnan(norm_value): - print("Unknown error. Norm computation resulted in NaN.") - return None + raise ct.ControlArgument("Norm computation resulted in NaN.") else: - return norm_value - # - # L_infinity-norm computation - # + return norm_value + + # --------------------------- + # L-infinity norm computation + # --------------------------- elif p == "inf": - def _Hamilton_matrix(gamma): - """Constructs Hamiltonian matrix. For internal use.""" - R = Ip*gamma**2 - D.T@D - invR = la.inv(R) - 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]]) - - # Discrete time case - # Use inverse bilinear transformation of discrete time system to s-plane if no poles on |z|=1 or z=0. - # Allows us to use test for continuous time systems next. - if G.isdtime(): - Ad = A - Bd = B - Cd = C - Dd = D - if any(np.isclose(abs(la.eigvals(Ad)), 1.0)): + + # Check for cases with infinite norm + poles = G.poles() + if G.isdtime(): # Discrete time + if any(np.isclose(abs(poles), 1.0)): # Poles on unit circle if print_warning: - print("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain.") + warnings.warn("Poles close to, or on, the complex unit circle. Norm value may be uncertain.") + return float('inf') + else: # Continuous time + if any(np.isclose(poles.real, 0.0)): # Poles on imaginary axis + if print_warning: + warnings.warn("Poles close to, or on, the imaginary axis. Norm value may be uncertain.") return float('inf') - elif any(np.isclose(la.eigvals(Ad), 0.0)): - print("L_infinity norm computation for discrete time system with pole(s) at z=0 currently not supported.") - return None - - # Inverse bilinear transformation - In = np.eye(len(Ad)) - Adinv = la.inv(Ad+In) - A = 2*(Ad-In)@Adinv - B = 2*Adinv@Bd - C = 2*Cd@Adinv - D = Dd - Cd@Adinv@Bd - - # Continus time case - if any(np.isclose(la.eigvals(A).real, 0.0)): - if print_warning: - print("Warning: Poles close to, or on, imaginary axis. Norm value may be uncertain.") - return float('inf') - gaml = la.norm(D,ord=2) # Lower bound - gamu = max(1.0, 2.0*gaml) # Candidate upper bound - Ip = np.eye(len(D)) - - while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find actual upper bound - gamu *= 2.0 + # Use slycot, if available, to compute (finite) norm + if ct.slycot_check() and use_slycot: + return ct.linfnorm(G, tol)[0] - while (gamu-gaml)/gamu > tol: - gam = (gamu+gaml)/2.0 - if any(np.isclose(la.eigvals(_Hamilton_matrix(gam)).real, 0.0)): - gaml = gam - else: - gamu = gam - return gam + # Else use scipy + else: + + # ------------------ + # Discrete time case + # ------------------ + # Use inverse bilinear transformation of discrete time system to s-plane if no poles on |z|=1 or z=0. + # Allows us to use test for continuous time systems next. + if G.isdtime(): + Ad = A + Bd = B + Cd = C + Dd = D + if any(np.isclose(la.eigvals(Ad), 0.0)): + raise ct.ControlArgument("L-infinity norm computation for discrete time system with pole(s) in z=0 currently not supported unless Slycot installed.") + + # Inverse bilinear transformation + In = np.eye(len(Ad)) + Adinv = la.inv(Ad+In) + A = 2*(Ad-In)@Adinv + B = 2*Adinv@Bd + C = 2*Cd@Adinv + D = Dd - Cd@Adinv@Bd + + # -------------------- + # Continuous time case + # -------------------- + def _Hamilton_matrix(gamma): + """Constructs Hamiltonian matrix. For internal use.""" + R = Ip*gamma**2 - D.T@D + invR = la.inv(R) + 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]]) + + gaml = la.norm(D,ord=2) # Lower bound + gamu = max(1.0, 2.0*gaml) # Candidate upper bound + Ip = np.eye(len(D)) + + while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find actual upper bound + gamu *= 2.0 + + while (gamu-gaml)/gamu > tol: + gam = (gamu+gaml)/2.0 + if any(np.isclose(la.eigvals(_Hamilton_matrix(gam)).real, 0.0)): + gaml = gam + else: + gamu = gam + return gam + + # ---------------------- + # Other norm computation + # ---------------------- else: - print(f"Norm computation for p={p} currently not supported.") - return None + raise ct.ControlArgument(f"Norm computation for p={p} currently not supported.") + From 5c38d4f3f7a29fe5e8c0c5a42ff105f6361af71d Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Mon, 22 Jan 2024 20:54:52 +0100 Subject: [PATCH 10/22] escape latex labels --- examples/genswitch.py | 2 +- examples/kincar-flatsys.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/genswitch.py b/examples/genswitch.py index e65e40110..58040cb3a 100644 --- a/examples/genswitch.py +++ b/examples/genswitch.py @@ -60,7 +60,7 @@ def genswitch(y, t, mu=4, n=2): # set(pl, 'LineWidth', AM_data_linewidth) plt.axis([0, 25, 0, 5]) -plt.xlabel('Time {\itt} [scaled]') +plt.xlabel('Time {\\itt} [scaled]') plt.ylabel('Protein concentrations [scaled]') plt.legend(('z1 (A)', 'z2 (B)')) # 'Orientation', 'horizontal') # legend(legh, 'boxoff') diff --git a/examples/kincar-flatsys.py b/examples/kincar-flatsys.py index b61a9e1c5..56b5672ee 100644 --- a/examples/kincar-flatsys.py +++ b/examples/kincar-flatsys.py @@ -100,8 +100,8 @@ def plot_results(t, x, ud, rescale=True): plt.subplot(2, 4, 8) plt.plot(t, ud[1]) - plt.xlabel('Ttime t [sec]') - plt.ylabel('$\delta$ [rad]') + plt.xlabel('Time t [sec]') + plt.ylabel('$\\delta$ [rad]') plt.tight_layout() # From 99e56f8f0dfb4a33600a08a0689828b0610472ec Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Mon, 22 Jan 2024 20:55:22 +0100 Subject: [PATCH 11/22] Use Numpy API for pi instead of undocumented scipy.pi --- examples/type2_type3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/type2_type3.py b/examples/type2_type3.py index 250aa266c..52e0645e2 100644 --- a/examples/type2_type3.py +++ b/examples/type2_type3.py @@ -5,7 +5,7 @@ import os import matplotlib.pyplot as plt # Grab MATLAB plotting functions from control.matlab import * # MATLAB-like functions -from scipy import pi +from numpy import pi integrator = tf([0, 1], [1, 0]) # 1/s # Parameters defining the system From 2c32913bf38285f24e554fd3b9892ae070d13044 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Mon, 22 Jan 2024 21:52:22 +0100 Subject: [PATCH 12/22] Update notebooks (no rerun, no output change) --- examples/bode-and-nyquist-plots.ipynb | 9 +++++---- examples/singular-values-plot.ipynb | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/bode-and-nyquist-plots.ipynb b/examples/bode-and-nyquist-plots.ipynb index 6ac74f34e..a38275a92 100644 --- a/examples/bode-and-nyquist-plots.ipynb +++ b/examples/bode-and-nyquist-plots.ipynb @@ -16,6 +16,7 @@ "metadata": {}, "outputs": [], "source": [ + "import numpy as np\n", "import scipy as sp\n", "import matplotlib.pyplot as plt\n", "import control as ct" @@ -109,9 +110,9 @@ "w001rad = 1. # 1 rad/s\n", "w010rad = 10. # 10 rad/s\n", "w100rad = 100. # 100 rad/s\n", - "w001hz = 2*sp.pi*1. # 1 Hz\n", - "w010hz = 2*sp.pi*10. # 10 Hz\n", - "w100hz = 2*sp.pi*100. # 100 Hz\n", + "w001hz = 2*np.pi*1. # 1 Hz\n", + "w010hz = 2*np.pi*10. # 10 Hz\n", + "w100hz = 2*np.pi*100. # 100 Hz\n", "# First order systems\n", "pt1_w001rad = ct.tf([1.], [1./w001rad, 1.], name='pt1_w001rad')\n", "display(pt1_w001rad)\n", @@ -153,7 +154,7 @@ ], "source": [ "sampleTime = 0.001\n", - "display('Nyquist frequency: {:.0f} Hz, {:.0f} rad/sec'.format(1./sampleTime /2., 2*sp.pi*1./sampleTime /2.))" + "display('Nyquist frequency: {:.0f} Hz, {:.0f} rad/sec'.format(1./sampleTime /2., 2*np.pi*1./sampleTime /2.))" ] }, { diff --git a/examples/singular-values-plot.ipynb b/examples/singular-values-plot.ipynb index f126c6c3f..676c76916 100644 --- a/examples/singular-values-plot.ipynb +++ b/examples/singular-values-plot.ipynb @@ -90,7 +90,7 @@ ], "source": [ "sampleTime = 10\n", - "display('Nyquist frequency: {:.4f} Hz, {:.4f} rad/sec'.format(1./sampleTime /2., 2*sp.pi*1./sampleTime /2.))" + "display('Nyquist frequency: {:.4f} Hz, {:.4f} rad/sec'.format(1./sampleTime /2., 2*np.pi*1./sampleTime /2.))" ] }, { From 7ace4bc6ba5422fafbe2f5af885b7ce450a13ffe Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Sun, 7 Jan 2024 21:53:32 +0100 Subject: [PATCH 13/22] New function for LTI system norm computation --- control/__init__.py | 1 + control/sysnorm.py | 75 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 control/sysnorm.py diff --git a/control/__init__.py b/control/__init__.py index 120d16325..5a9e05e95 100644 --- a/control/__init__.py +++ b/control/__init__.py @@ -101,6 +101,7 @@ from .config import * from .sisotool import * from .passivity import * +from .sysnorm import * # Exceptions from .exception import * diff --git a/control/sysnorm.py b/control/sysnorm.py new file mode 100644 index 000000000..5caae0918 --- /dev/null +++ b/control/sysnorm.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Dec 21 08:06:12 2023 + +@author: hsan +""" + +import numpy as np +import numpy.linalg as la + +import control as ct + +#------------------------------------------------------------------------------ + +def norm(system, p=2, tol=1e-6): + """Computes H_2 (p=2) or L_infinity (p="inf", tolerance tol) norm of system.""" + G = ct.ss(system) + A = G.A + B = G.B + C = G.C + D = G.D + + if p == 2: # H_2-norm + if G.isctime(): + if (D != 0).any() or any(G.poles().real >= 0): + return float('inf') + else: + P = ct.lyap(A, B@B.T) + return np.sqrt(np.trace(C@P@C.T)) + elif G.isdtime(): + if any(abs(G.poles()) >= 1): + return float('inf') + else: + P = ct.dlyap(A, B@B.T) + return np.sqrt(np.trace(C@P@C.T + D@D.T)) + + elif p == "inf": # L_infinity-norm + def Hamilton_matrix(gamma): + """Constructs Hamiltonian matrix.""" + R = Ip*gamma**2 - D.T@D + invR = la.inv(R) + 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]]) + + if G.isdtime(): # Bilinear transformation to s-plane + Ad = A + Bd = B + Cd = C + Dd = D + In = np.eye(len(Ad)) + Adinv = la.inv(Ad+In) + A = 2*(Ad-In)@Adinv + B = 2*Adinv@Bd + C = 2*Cd@Adinv + D = Dd - Cd@Adinv@Bd + + if any(np.isclose(la.eigvals(A).real, 0.0)): + return float('inf') + + gaml = la.norm(D,ord=2) # Lower bound + gamu = max(1.0, 2.0*gaml) # Candidate upper bound + Ip = np.eye(len(D)) + + while any(np.isclose(la.eigvals(Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound + gamu *= 2.0 + + while (gamu-gaml)/gamu > tol: + gam = (gamu+gaml)/2.0 + if any(np.isclose(la.eigvals(Hamilton_matrix(gam)).real, 0.0)): + gaml = gam + else: + gamu = gam + return gam + else: + # Norm computation only supported for p=2 and p='inf' + return None From 510344812f027ddfa4b65a65f7461bcee9c577fc Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Mon, 8 Jan 2024 13:51:33 +0100 Subject: [PATCH 14/22] * Updated documentation of function norm * Added control/tests/sysnorm_test.py --- control/sysnorm.py | 57 ++++++++++++++++++++++++++----- control/tests/sysnorm_test.py | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 control/tests/sysnorm_test.py diff --git a/control/sysnorm.py b/control/sysnorm.py index 5caae0918..074055254 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -1,8 +1,14 @@ # -*- coding: utf-8 -*- -""" -Created on Thu Dec 21 08:06:12 2023 +"""sysnorm.py + +Functions for computing system norms. -@author: hsan +Routines in this module: + +norm() + +Created on Thu Dec 21 08:06:12 2023 +Author: Henrik Sandberg """ import numpy as np @@ -13,7 +19,35 @@ #------------------------------------------------------------------------------ def norm(system, p=2, tol=1e-6): - """Computes H_2 (p=2) or L_infinity (p="inf", tolerance tol) norm of system.""" + """Computes norm of system. + + Parameters + ---------- + system : LTI (:class:`StateSpace` or :class:`TransferFunction`) + System in continuous or discrete time for which the norm should be computed. + p : int or str + Type of norm to be computed. p=2 gives the H_2 norm, and p='inf' gives the L_infinity norm. + tol : float + Relative tolerance for accuracy of L_infinity norm computation. Ignored + unless p='inf'. + + Returns + ------- + norm : float + Norm of system + + Notes + ----- + Does not yet compute the L_infinity norm for discrete time systems with pole(s) in z=0. + + Examples + -------- + >>> Gc = ct.tf([1], [1, 2, 1]) + >>> ct.norm(Gc,2) + 0.5000000000000001 + >>> ct.norm(Gc,'inf',tol=1e-10) + 1.0000000000582077 + """ G = ct.ss(system) A = G.A B = G.B @@ -35,17 +69,22 @@ def norm(system, p=2, tol=1e-6): return np.sqrt(np.trace(C@P@C.T + D@D.T)) elif p == "inf": # L_infinity-norm - def Hamilton_matrix(gamma): + def _Hamilton_matrix(gamma): """Constructs Hamiltonian matrix.""" R = Ip*gamma**2 - D.T@D invR = la.inv(R) 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]]) - if G.isdtime(): # Bilinear transformation to s-plane + if G.isdtime(): # Bilinear transformation of discrete time system to s-plane if no poles at |z|=1 or z=0 Ad = A Bd = B Cd = C Dd = D + if any(np.isclose(abs(la.eigvals(Ad)), 1.0)): + return float('inf') + elif any(np.isclose(la.eigvals(Ad), 0.0)): + print("L_infinity norm computation for discrete time system with pole(s) at z = 0 currently not supported.") + return None In = np.eye(len(Ad)) Adinv = la.inv(Ad+In) A = 2*(Ad-In)@Adinv @@ -60,16 +99,16 @@ def Hamilton_matrix(gamma): gamu = max(1.0, 2.0*gaml) # Candidate upper bound Ip = np.eye(len(D)) - while any(np.isclose(la.eigvals(Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound + while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound gamu *= 2.0 while (gamu-gaml)/gamu > tol: gam = (gamu+gaml)/2.0 - if any(np.isclose(la.eigvals(Hamilton_matrix(gam)).real, 0.0)): + if any(np.isclose(la.eigvals(_Hamilton_matrix(gam)).real, 0.0)): gaml = gam else: gamu = gam return gam else: - # Norm computation only supported for p=2 and p='inf' + print("Norm computation for p =", p, "currently not supported.") return None diff --git a/control/tests/sysnorm_test.py b/control/tests/sysnorm_test.py new file mode 100644 index 000000000..915e64622 --- /dev/null +++ b/control/tests/sysnorm_test.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +""" +Tests for sysnorm module. + +Created on Mon Jan 8 11:31:46 2024 +Author: Henrik Sandberg +""" + +import control as ct +import numpy as np + +def test_norm_1st_order_stable_system(): + """First-order stable continuous-time system""" + s = ct.tf('s') + G1 = 1/(s+1) + assert np.allclose(ct.norm(G1, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G1, p=2), 0.707106781186547, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + + Gd1 = ct.sample_system(G1, 0.1) + assert np.allclose(ct.norm(Gd1, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd1, p=2), 0.223513699524858, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + + +def test_norm_1st_order_unstable_system(): + """First-order unstable continuous-time system""" + s = ct.tf('s') + G2 = 1/(1-s) + assert np.allclose(ct.norm(G2, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert ct.norm(G2, p=2) == float('inf') # Comparison to norm computed in MATLAB + + Gd2 = ct.sample_system(G2, 0.1) + assert np.allclose(ct.norm(Gd2, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert ct.norm(Gd2, p=2) == float('inf') # Comparison to norm computed in MATLAB + +def test_norm_2nd_order_system_imag_poles(): + """Second-order continuous-time system with poles on imaginary axis""" + s = ct.tf('s') + G3 = 1/(s**2+1) + assert ct.norm(G3, p='inf') == float('inf') # Comparison to norm computed in MATLAB + assert ct.norm(G3, p=2) == float('inf') # Comparison to norm computed in MATLAB + + Gd3 = ct.sample_system(G3, 0.1) + assert ct.norm(Gd3, p='inf') == float('inf') # Comparison to norm computed in MATLAB + assert ct.norm(Gd3, p=2) == float('inf') # Comparison to norm computed in MATLAB + +def test_norm_3rd_order_mimo_system(): + """Third-order stable MIMO continuous-time system""" + A = np.array([[-1.017041847539126, -0.224182952826418, 0.042538079149249], + [-0.310374015319095, -0.516461581407780, -0.119195790221750], + [-1.452723568727942, 1.7995860837102088, -1.491935830615152]]) + B = np.array([[0.312858596637428, -0.164879019209038], + [-0.864879917324456, 0.627707287528727], + [-0.030051296196269, 1.093265669039484]]) + C = np.array([[1.109273297614398, 0.077359091130425, -1.113500741486764], + [-0.863652821988714, -1.214117043615409, -0.006849328103348]]) + D = np.zeros((2,2)) + G4 = ct.ss(A,B,C,D) # Random system generated in MATLAB + assert np.allclose(ct.norm(G4, p='inf', tol=1e-9), 4.276759162964244, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G4, p=2), 2.237461821810309, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + + Gd4 = ct.sample_system(G4, 0.1) + assert np.allclose(ct.norm(Gd4, p='inf', tol=1e-9), 4.276759162964228, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd4, p=2), 0.707434962289554, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB From 4fb252eb602c01a522469013fe6eb04dedd625e1 Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Tue, 9 Jan 2024 16:50:45 +0100 Subject: [PATCH 15/22] Update sysnorm.py * Added additional tests and warning messages for systems with poles close to stability boundary * Added __all__ * Added more comments --- control/sysnorm.py | 110 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 22 deletions(-) diff --git a/control/sysnorm.py b/control/sysnorm.py index 074055254..2065f8721 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -3,9 +3,9 @@ Functions for computing system norms. -Routines in this module: +Routine in this module: -norm() +norm Created on Thu Dec 21 08:06:12 2023 Author: Henrik Sandberg @@ -16,9 +16,11 @@ import control as ct +__all__ = ['norm'] + #------------------------------------------------------------------------------ -def norm(system, p=2, tol=1e-6): +def norm(system, p=2, tol=1e-6, print_warning=True): """Computes norm of system. Parameters @@ -30,11 +32,13 @@ def norm(system, p=2, tol=1e-6): tol : float Relative tolerance for accuracy of L_infinity norm computation. Ignored unless p='inf'. + print_warning : bool + Print warning message in case norm value may be uncertain. Returns ------- - norm : float - Norm of system + norm_value : float or NoneType + Norm value of system (float) or None if computation could not be completed. Notes ----- @@ -54,52 +58,114 @@ def norm(system, p=2, tol=1e-6): C = G.C D = G.D - if p == 2: # H_2-norm + # + # H_2-norm computation + # + if p == 2: + # Continuous time case if G.isctime(): - if (D != 0).any() or any(G.poles().real >= 0): + poles_real_part = G.poles().real + if any(np.isclose(poles_real_part, 0.0)): + if print_warning: + print("Warning: Poles close to, or on, the imaginary axis. Norm value may be uncertain.") + return float('inf') + elif (D != 0).any() or any(poles_real_part > 0.0): # System unstable or has direct feedthrough? return float('inf') else: - P = ct.lyap(A, B@B.T) - return np.sqrt(np.trace(C@P@C.T)) + try: + P = ct.lyap(A, B@B.T) + except Exception as e: + print(f"An error occurred solving the continuous time Lyapunov equation: {e}") + return None + + # System is stable to reach this point, and P should be positive semi-definite. + # Test next is a precaution in case the Lyapunov equation is ill conditioned. + if any(la.eigvals(P) < 0.0): + if print_warning: + print("Warning: There appears to be poles close to the imaginary axis. Norm value may be uncertain.") + return float('inf') + else: + norm_value = np.sqrt(np.trace(C@P@C.T)) # Argument in sqrt should be non-negative + if np.isnan(norm_value): + print("Unknown error. Norm computation resulted in NaN.") + return None + else: + return norm_value + + # Discrete time case elif G.isdtime(): - if any(abs(G.poles()) >= 1): + poles_abs = abs(G.poles()) + if any(np.isclose(poles_abs, 1.0)): + if print_warning: + print("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain.") + return float('inf') + elif any(poles_abs > 1.0): # System unstable? return float('inf') else: - P = ct.dlyap(A, B@B.T) - return np.sqrt(np.trace(C@P@C.T + D@D.T)) - - elif p == "inf": # L_infinity-norm + try: + P = ct.dlyap(A, B@B.T) + except Exception as e: + print(f"An error occurred solving the discrete time Lyapunov equation: {e}") + return None + + # System is stable to reach this point, and P should be positive semi-definite. + # Test next is a precaution in case the Lyapunov equation is ill conditioned. + if any(la.eigvals(P) < 0.0): + if print_warning: + print("Warning: There appears to be poles close to the complex unit circle. Norm value may be uncertain.") + return float('inf') + else: + norm_value = np.sqrt(np.trace(C@P@C.T + D@D.T)) # Argument in sqrt should be non-negative + if np.isnan(norm_value): + print("Unknown error. Norm computation resulted in NaN.") + return None + else: + return norm_value + # + # L_infinity-norm computation + # + elif p == "inf": def _Hamilton_matrix(gamma): - """Constructs Hamiltonian matrix.""" + """Constructs Hamiltonian matrix. For internal use.""" R = Ip*gamma**2 - D.T@D invR = la.inv(R) 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]]) - if G.isdtime(): # Bilinear transformation of discrete time system to s-plane if no poles at |z|=1 or z=0 + # Discrete time case + # Use inverse bilinear transformation of discrete time system to s-plane if no poles on |z|=1 or z=0. + # Allows us to use test for continuous time systems next. + if G.isdtime(): Ad = A Bd = B Cd = C Dd = D if any(np.isclose(abs(la.eigvals(Ad)), 1.0)): + if print_warning: + print("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain.") return float('inf') elif any(np.isclose(la.eigvals(Ad), 0.0)): - print("L_infinity norm computation for discrete time system with pole(s) at z = 0 currently not supported.") + print("L_infinity norm computation for discrete time system with pole(s) at z=0 currently not supported.") return None + + # Inverse bilinear transformation In = np.eye(len(Ad)) Adinv = la.inv(Ad+In) A = 2*(Ad-In)@Adinv B = 2*Adinv@Bd C = 2*Cd@Adinv D = Dd - Cd@Adinv@Bd - + + # Continus time case if any(np.isclose(la.eigvals(A).real, 0.0)): + if print_warning: + print("Warning: Poles close to, or on, imaginary axis. Norm value may be uncertain.") return float('inf') - gaml = la.norm(D,ord=2) # Lower bound - gamu = max(1.0, 2.0*gaml) # Candidate upper bound + gaml = la.norm(D,ord=2) # Lower bound + gamu = max(1.0, 2.0*gaml) # Candidate upper bound Ip = np.eye(len(D)) - while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find an upper bound + while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find actual upper bound gamu *= 2.0 while (gamu-gaml)/gamu > tol: @@ -110,5 +176,5 @@ def _Hamilton_matrix(gamma): gamu = gam return gam else: - print("Norm computation for p =", p, "currently not supported.") + print(f"Norm computation for p={p} currently not supported.") return None From 108817ce9c006ac14dde3198f796f8c631fc81cd Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Sun, 14 Jan 2024 13:03:21 +0100 Subject: [PATCH 16/22] Do not track changes in VS Code setup. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1b10a3585..4a6aa3cc0 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ TAGS # Files created by Spyder .spyproject/ +# Files created by or for VS Code (HS, 13 Jan, 2024) +.vscode/ + # Environments .env .venv From 6683eb3087850df373917ca8373d0c060a5d0d32 Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Sun, 21 Jan 2024 19:23:12 +0100 Subject: [PATCH 17/22] Lowered tolerances in tests. --- control/tests/sysnorm_test.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/control/tests/sysnorm_test.py b/control/tests/sysnorm_test.py index 915e64622..917e98d04 100644 --- a/control/tests/sysnorm_test.py +++ b/control/tests/sysnorm_test.py @@ -13,23 +13,23 @@ def test_norm_1st_order_stable_system(): """First-order stable continuous-time system""" s = ct.tf('s') G1 = 1/(s+1) - assert np.allclose(ct.norm(G1, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB - assert np.allclose(ct.norm(G1, p=2), 0.707106781186547, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G1, p='inf', tol=1e-9), 1.0) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G1, p=2), 0.707106781186547) # Comparison to norm computed in MATLAB Gd1 = ct.sample_system(G1, 0.1) - assert np.allclose(ct.norm(Gd1, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB - assert np.allclose(ct.norm(Gd1, p=2), 0.223513699524858, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd1, p='inf', tol=1e-9), 1.0) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd1, p=2), 0.223513699524858) # Comparison to norm computed in MATLAB def test_norm_1st_order_unstable_system(): """First-order unstable continuous-time system""" s = ct.tf('s') G2 = 1/(1-s) - assert np.allclose(ct.norm(G2, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G2, p='inf', tol=1e-9), 1.0) # Comparison to norm computed in MATLAB assert ct.norm(G2, p=2) == float('inf') # Comparison to norm computed in MATLAB Gd2 = ct.sample_system(G2, 0.1) - assert np.allclose(ct.norm(Gd2, p='inf', tol=1e-9), 1.0, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd2, p='inf', tol=1e-9), 1.0) # Comparison to norm computed in MATLAB assert ct.norm(Gd2, p=2) == float('inf') # Comparison to norm computed in MATLAB def test_norm_2nd_order_system_imag_poles(): @@ -55,9 +55,9 @@ def test_norm_3rd_order_mimo_system(): [-0.863652821988714, -1.214117043615409, -0.006849328103348]]) D = np.zeros((2,2)) G4 = ct.ss(A,B,C,D) # Random system generated in MATLAB - assert np.allclose(ct.norm(G4, p='inf', tol=1e-9), 4.276759162964244, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB - assert np.allclose(ct.norm(G4, p=2), 2.237461821810309, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G4, p='inf', tol=1e-9), 4.276759162964244) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(G4, p=2), 2.237461821810309) # Comparison to norm computed in MATLAB Gd4 = ct.sample_system(G4, 0.1) - assert np.allclose(ct.norm(Gd4, p='inf', tol=1e-9), 4.276759162964228, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB - assert np.allclose(ct.norm(Gd4, p=2), 0.707434962289554, rtol=1e-09, atol=1e-08) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd4, p='inf', tol=1e-9), 4.276759162964228) # Comparison to norm computed in MATLAB + assert np.allclose(ct.norm(Gd4, p=2), 0.707434962289554) # Comparison to norm computed in MATLAB From 32d38bfec40589117d0be3c0b3cae19c9369f34a Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Sun, 21 Jan 2024 19:29:32 +0100 Subject: [PATCH 18/22] Added: * Use of warnings package. * Use routine statesp.linfnorm when slycot installed. * New routine internal _h2norm_slycot when slycot is installed. --- control/sysnorm.py | 285 +++++++++++++++++++++++++++++++-------------- 1 file changed, 195 insertions(+), 90 deletions(-) diff --git a/control/sysnorm.py b/control/sysnorm.py index 2065f8721..a25ef305f 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -12,7 +12,9 @@ """ import numpy as np +import scipy as sp import numpy.linalg as la +import warnings import control as ct @@ -20,7 +22,68 @@ #------------------------------------------------------------------------------ -def norm(system, p=2, tol=1e-6, print_warning=True): +def _h2norm_slycot(sys, print_warning=True): + """H2 norm of a linear system. For internal use. Requires Slycot. + + See also + -------- + slycot.ab13bd : the Slycot routine that does the calculation + https://github.com/python-control/Slycot/issues/199 : Post on issue with ab13bf + """ + + try: + from slycot import ab13bd + except ImportError: + ct.ControlSlycot("Can't find slycot module 'ab13bd'!") + + try: + from slycot.exceptions import SlycotArithmeticError + except ImportError: + raise ct.ControlSlycot("Can't find slycot class 'SlycotArithmeticError'!") + + A, B, C, D = ct.ssdata(ct.ss(sys)) + + n = A.shape[0] + m = B.shape[1] + p = C.shape[0] + + dico = 'C' if sys.isctime() else 'D' # Continuous or discrete time + jobn = 'H' # H2 (and not L2 norm) + + if n == 0: + # ab13bd does not accept empty A, B, C + if dico == 'C': + if any(D.flat != 0): + if print_warning: + warnings.warn("System has a direct feedthrough term!") + return float("inf") + else: + return 0.0 + elif dico == 'D': + return np.sqrt(D@D.T) + + try: + norm = ab13bd(dico, jobn, n, m, p, A, B, C, D) + except SlycotArithmeticError as e: + if e.info == 3: + if print_warning: + warnings.warn("System has pole(s) on the stability boundary!") + return float("inf") + elif e.info == 5: + if print_warning: + warnings.warn("System has a direct feedthrough term!") + return float("inf") + elif e.info == 6: + if print_warning: + warnings.warn("System is unstable!") + return float("inf") + else: + raise e + return norm + +#------------------------------------------------------------------------------ + +def norm(system, p=2, tol=1e-10, print_warning=True, use_slycot=True): """Computes norm of system. Parameters @@ -28,12 +91,14 @@ def norm(system, p=2, tol=1e-6, print_warning=True): system : LTI (:class:`StateSpace` or :class:`TransferFunction`) System in continuous or discrete time for which the norm should be computed. p : int or str - Type of norm to be computed. p=2 gives the H_2 norm, and p='inf' gives the L_infinity norm. + Type of norm to be computed. p=2 gives the H2 norm, and p='inf' gives the L-infinity norm. tol : float - Relative tolerance for accuracy of L_infinity norm computation. Ignored + Relative tolerance for accuracy of L-infinity norm computation. Ignored unless p='inf'. print_warning : bool Print warning message in case norm value may be uncertain. + use_slycot : bool + Use Slycot routines if available. Returns ------- @@ -42,7 +107,7 @@ def norm(system, p=2, tol=1e-6, print_warning=True): Notes ----- - Does not yet compute the L_infinity norm for discrete time systems with pole(s) in z=0. + Does not yet compute the L-infinity norm for discrete time systems with pole(s) in z=0 unless Slycot is used. Examples -------- @@ -58,123 +123,163 @@ def norm(system, p=2, tol=1e-6, print_warning=True): C = G.C D = G.D - # - # H_2-norm computation - # + # ------------------- + # H2 norm computation + # ------------------- if p == 2: + # -------------------- # Continuous time case + # -------------------- if G.isctime(): + + # Check for cases with infinite norm poles_real_part = G.poles().real - if any(np.isclose(poles_real_part, 0.0)): + if any(np.isclose(poles_real_part, 0.0)): # Poles on imaginary axis if print_warning: - print("Warning: Poles close to, or on, the imaginary axis. Norm value may be uncertain.") + warnings.warn("Poles close to, or on, the imaginary axis. Norm value may be uncertain.") return float('inf') - elif (D != 0).any() or any(poles_real_part > 0.0): # System unstable or has direct feedthrough? + elif any(poles_real_part > 0.0): # System unstable + if print_warning: + warnings.warn("System is unstable!") return float('inf') - else: - try: - P = ct.lyap(A, B@B.T) - except Exception as e: - print(f"An error occurred solving the continuous time Lyapunov equation: {e}") - return None + elif any(D.flat != 0): # System has direct feedthrough + if print_warning: + warnings.warn("System has a direct feedthrough term!") + return float('inf') + + else: + # Use slycot, if available, to compute (finite) norm + if ct.slycot_check() and use_slycot: + return _h2norm_slycot(G, print_warning) - # System is stable to reach this point, and P should be positive semi-definite. - # Test next is a precaution in case the Lyapunov equation is ill conditioned. - if any(la.eigvals(P) < 0.0): - if print_warning: - print("Warning: There appears to be poles close to the imaginary axis. Norm value may be uncertain.") - return float('inf') + # Else use scipy else: - norm_value = np.sqrt(np.trace(C@P@C.T)) # Argument in sqrt should be non-negative - if np.isnan(norm_value): - print("Unknown error. Norm computation resulted in NaN.") - return None + P = ct.lyap(A, B@B.T) # Solve for controllability Gramian + + # System is stable to reach this point, and P should be positive semi-definite. + # Test next is a precaution in case the Lyapunov equation is ill conditioned. + if any(la.eigvals(P).real < 0.0): + if print_warning: + warnings.warn("There appears to be poles close to the imaginary axis. Norm value may be uncertain.") + return float('inf') else: - return norm_value + norm_value = np.sqrt(np.trace(C@P@C.T)) # Argument in sqrt should be non-negative + if np.isnan(norm_value): + raise ct.ControlArgument("Norm computation resulted in NaN.") + else: + return norm_value + # ------------------ # Discrete time case + # ------------------ elif G.isdtime(): + + # Check for cases with infinite norm poles_abs = abs(G.poles()) - if any(np.isclose(poles_abs, 1.0)): + if any(np.isclose(poles_abs, 1.0)): # Poles on imaginary axis if print_warning: - print("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain.") + warnings.warn("Poles close to, or on, the complex unit circle. Norm value may be uncertain.") return float('inf') - elif any(poles_abs > 1.0): # System unstable? + elif any(poles_abs > 1.0): # System unstable + if print_warning: + warnings.warn("System is unstable!") return float('inf') + else: - try: + # Use slycot, if available, to compute (finite) norm + if ct.slycot_check() and use_slycot: + return _h2norm_slycot(G, print_warning) + + # Else use scipy + else: P = ct.dlyap(A, B@B.T) - except Exception as e: - print(f"An error occurred solving the discrete time Lyapunov equation: {e}") - return None # System is stable to reach this point, and P should be positive semi-definite. # Test next is a precaution in case the Lyapunov equation is ill conditioned. - if any(la.eigvals(P) < 0.0): + if any(la.eigvals(P).real < 0.0): if print_warning: - print("Warning: There appears to be poles close to the complex unit circle. Norm value may be uncertain.") + warnings.warn("Warning: There appears to be poles close to the complex unit circle. Norm value may be uncertain.") return float('inf') else: norm_value = np.sqrt(np.trace(C@P@C.T + D@D.T)) # Argument in sqrt should be non-negative if np.isnan(norm_value): - print("Unknown error. Norm computation resulted in NaN.") - return None + raise ct.ControlArgument("Norm computation resulted in NaN.") else: - return norm_value - # - # L_infinity-norm computation - # + return norm_value + + # --------------------------- + # L-infinity norm computation + # --------------------------- elif p == "inf": - def _Hamilton_matrix(gamma): - """Constructs Hamiltonian matrix. For internal use.""" - R = Ip*gamma**2 - D.T@D - invR = la.inv(R) - 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]]) - - # Discrete time case - # Use inverse bilinear transformation of discrete time system to s-plane if no poles on |z|=1 or z=0. - # Allows us to use test for continuous time systems next. - if G.isdtime(): - Ad = A - Bd = B - Cd = C - Dd = D - if any(np.isclose(abs(la.eigvals(Ad)), 1.0)): + + # Check for cases with infinite norm + poles = G.poles() + if G.isdtime(): # Discrete time + if any(np.isclose(abs(poles), 1.0)): # Poles on unit circle if print_warning: - print("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain.") + warnings.warn("Poles close to, or on, the complex unit circle. Norm value may be uncertain.") + return float('inf') + else: # Continuous time + if any(np.isclose(poles.real, 0.0)): # Poles on imaginary axis + if print_warning: + warnings.warn("Poles close to, or on, the imaginary axis. Norm value may be uncertain.") return float('inf') - elif any(np.isclose(la.eigvals(Ad), 0.0)): - print("L_infinity norm computation for discrete time system with pole(s) at z=0 currently not supported.") - return None - - # Inverse bilinear transformation - In = np.eye(len(Ad)) - Adinv = la.inv(Ad+In) - A = 2*(Ad-In)@Adinv - B = 2*Adinv@Bd - C = 2*Cd@Adinv - D = Dd - Cd@Adinv@Bd - - # Continus time case - if any(np.isclose(la.eigvals(A).real, 0.0)): - if print_warning: - print("Warning: Poles close to, or on, imaginary axis. Norm value may be uncertain.") - return float('inf') - gaml = la.norm(D,ord=2) # Lower bound - gamu = max(1.0, 2.0*gaml) # Candidate upper bound - Ip = np.eye(len(D)) - - while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find actual upper bound - gamu *= 2.0 + # Use slycot, if available, to compute (finite) norm + if ct.slycot_check() and use_slycot: + return ct.linfnorm(G, tol)[0] - while (gamu-gaml)/gamu > tol: - gam = (gamu+gaml)/2.0 - if any(np.isclose(la.eigvals(_Hamilton_matrix(gam)).real, 0.0)): - gaml = gam - else: - gamu = gam - return gam + # Else use scipy + else: + + # ------------------ + # Discrete time case + # ------------------ + # Use inverse bilinear transformation of discrete time system to s-plane if no poles on |z|=1 or z=0. + # Allows us to use test for continuous time systems next. + if G.isdtime(): + Ad = A + Bd = B + Cd = C + Dd = D + if any(np.isclose(la.eigvals(Ad), 0.0)): + raise ct.ControlArgument("L-infinity norm computation for discrete time system with pole(s) in z=0 currently not supported unless Slycot installed.") + + # Inverse bilinear transformation + In = np.eye(len(Ad)) + Adinv = la.inv(Ad+In) + A = 2*(Ad-In)@Adinv + B = 2*Adinv@Bd + C = 2*Cd@Adinv + D = Dd - Cd@Adinv@Bd + + # -------------------- + # Continuous time case + # -------------------- + def _Hamilton_matrix(gamma): + """Constructs Hamiltonian matrix. For internal use.""" + R = Ip*gamma**2 - D.T@D + invR = la.inv(R) + 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]]) + + gaml = la.norm(D,ord=2) # Lower bound + gamu = max(1.0, 2.0*gaml) # Candidate upper bound + Ip = np.eye(len(D)) + + while any(np.isclose(la.eigvals(_Hamilton_matrix(gamu)).real, 0.0)): # Find actual upper bound + gamu *= 2.0 + + while (gamu-gaml)/gamu > tol: + gam = (gamu+gaml)/2.0 + if any(np.isclose(la.eigvals(_Hamilton_matrix(gam)).real, 0.0)): + gaml = gam + else: + gamu = gam + return gam + + # ---------------------- + # Other norm computation + # ---------------------- else: - print(f"Norm computation for p={p} currently not supported.") - return None + raise ct.ControlArgument(f"Norm computation for p={p} currently not supported.") + From 6f810ba623c82e8bb34e7ca2db930274ce89c8ce Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Tue, 9 Jan 2024 16:50:45 +0100 Subject: [PATCH 19/22] Update sysnorm.py * Added additional tests and warning messages for systems with poles close to stability boundary * Added __all__ * Added more comments --- control/sysnorm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/control/sysnorm.py b/control/sysnorm.py index a25ef305f..7b4ba52da 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -20,6 +20,8 @@ __all__ = ['norm'] +__all__ = ['norm'] + #------------------------------------------------------------------------------ def _h2norm_slycot(sys, print_warning=True): From 793f0d659d75cb7c1d03e50fdb2092122c43513c Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Sun, 28 Jan 2024 15:57:32 +0100 Subject: [PATCH 20/22] Added: * type check when calling ct.norm * metod argument in ct.norm (slycot or scipy) --- control/sysnorm.py | 55 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/control/sysnorm.py b/control/sysnorm.py index 7b4ba52da..0db76c32a 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -24,24 +24,36 @@ #------------------------------------------------------------------------------ +def _slycot_or_scipy(method): + """ Copied from ct.mateqn. For internal use.""" + + if method == 'slycot' or (method is None and ct.slycot_check()): + return 'slycot' + elif method == 'scipy' or (method is None and not ct.slycot_check()): + return 'scipy' + else: + raise ct.ControlArgument(f"Unknown argument '{method}'.") + +#------------------------------------------------------------------------------ + def _h2norm_slycot(sys, print_warning=True): """H2 norm of a linear system. For internal use. Requires Slycot. See also -------- - slycot.ab13bd : the Slycot routine that does the calculation - https://github.com/python-control/Slycot/issues/199 : Post on issue with ab13bf + ``slycot.ab13bd`` : the Slycot routine that does the calculation + https://github.com/python-control/Slycot/issues/199 : Post on issue with ``ab13bf`` """ try: from slycot import ab13bd except ImportError: - ct.ControlSlycot("Can't find slycot module 'ab13bd'!") + ct.ControlSlycot("Can't find slycot module ``ab13bd``!") try: from slycot.exceptions import SlycotArithmeticError except ImportError: - raise ct.ControlSlycot("Can't find slycot class 'SlycotArithmeticError'!") + raise ct.ControlSlycot("Can't find slycot class ``SlycotArithmeticError``!") A, B, C, D = ct.ssdata(ct.ss(sys)) @@ -85,7 +97,7 @@ def _h2norm_slycot(sys, print_warning=True): #------------------------------------------------------------------------------ -def norm(system, p=2, tol=1e-10, print_warning=True, use_slycot=True): +def norm(system, p=2, tol=1e-10, print_warning=True, method=None): """Computes norm of system. Parameters @@ -99,13 +111,15 @@ def norm(system, p=2, tol=1e-10, print_warning=True, use_slycot=True): unless p='inf'. print_warning : bool Print warning message in case norm value may be uncertain. - use_slycot : bool - Use Slycot routines if available. + method : str, optional + Set the method used for computing the result. Current methods are + 'slycot' and 'scipy'. If set to None (default), try 'slycot' first + and then 'scipy'. Returns ------- - norm_value : float or NoneType - Norm value of system (float) or None if computation could not be completed. + norm_value : float + Norm value of system. Notes ----- @@ -114,17 +128,24 @@ def norm(system, p=2, tol=1e-10, print_warning=True, use_slycot=True): Examples -------- >>> Gc = ct.tf([1], [1, 2, 1]) - >>> ct.norm(Gc,2) + >>> ct.norm(Gc, 2) 0.5000000000000001 - >>> ct.norm(Gc,'inf',tol=1e-10) - 1.0000000000582077 + >>> ct.norm(Gc, 'inf', tol=1e-11, method='scipy') + 1.000000000007276 """ + + if not isinstance(system, (ct.StateSpace, ct.TransferFunction)): + raise TypeError('Parameter ``system``: must be a ``StateSpace`` or ``TransferFunction``') + G = ct.ss(system) A = G.A B = G.B C = G.C D = G.D + # Decide what method to use + method = _slycot_or_scipy(method) + # ------------------- # H2 norm computation # ------------------- @@ -151,12 +172,12 @@ def norm(system, p=2, tol=1e-10, print_warning=True, use_slycot=True): else: # Use slycot, if available, to compute (finite) norm - if ct.slycot_check() and use_slycot: + if method == 'slycot': return _h2norm_slycot(G, print_warning) # Else use scipy else: - P = ct.lyap(A, B@B.T) # Solve for controllability Gramian + P = ct.lyap(A, B@B.T, method=method) # Solve for controllability Gramian # System is stable to reach this point, and P should be positive semi-definite. # Test next is a precaution in case the Lyapunov equation is ill conditioned. @@ -189,12 +210,12 @@ def norm(system, p=2, tol=1e-10, print_warning=True, use_slycot=True): else: # Use slycot, if available, to compute (finite) norm - if ct.slycot_check() and use_slycot: + if method == 'slycot': return _h2norm_slycot(G, print_warning) # Else use scipy else: - P = ct.dlyap(A, B@B.T) + P = ct.dlyap(A, B@B.T, method=method) # System is stable to reach this point, and P should be positive semi-definite. # Test next is a precaution in case the Lyapunov equation is ill conditioned. @@ -228,7 +249,7 @@ def norm(system, p=2, tol=1e-10, print_warning=True, use_slycot=True): return float('inf') # Use slycot, if available, to compute (finite) norm - if ct.slycot_check() and use_slycot: + if method == 'slycot': return ct.linfnorm(G, tol)[0] # Else use scipy From 7a5af505c4996056b033a915e0171ea9d132a5dc Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Sun, 28 Jan 2024 16:34:18 +0100 Subject: [PATCH 21/22] Fixed merge error with __all__. --- control/sysnorm.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/control/sysnorm.py b/control/sysnorm.py index 0db76c32a..547f01f79 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -20,8 +20,6 @@ __all__ = ['norm'] -__all__ = ['norm'] - #------------------------------------------------------------------------------ def _slycot_or_scipy(method): From 49f7e5f5d863eb583a28ca12a3f61181f585499a Mon Sep 17 00:00:00 2001 From: Henrik Sandberg Date: Tue, 9 Jan 2024 16:50:45 +0100 Subject: [PATCH 22/22] Update sysnorm.py * Added additional tests and warning messages for systems with poles close to stability boundary * Added __all__ * Added more comments --- control/sysnorm.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/control/sysnorm.py b/control/sysnorm.py index 0db76c32a..547f01f79 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -20,8 +20,6 @@ __all__ = ['norm'] -__all__ = ['norm'] - #------------------------------------------------------------------------------ def _slycot_or_scipy(method):