Skip to content

Commit dd95f35

Browse files
authored
Merge pull request #729 from roryyorke/rory/linfnorm
ENH: add `linform` to compute linear system L-infinity norm
2 parents 46b3c53 + 02b3451 commit dd95f35

File tree

2 files changed

+117
-2
lines changed

2 files changed

+117
-2
lines changed

control/statesp.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from numpy.linalg import solve, eigvals, matrix_rank
5656
from numpy.linalg.linalg import LinAlgError
5757
import scipy as sp
58+
import scipy.linalg
5859
from scipy.signal import cont2discrete
5960
from scipy.signal import StateSpace as signalStateSpace
6061
from warnings import warn
@@ -65,7 +66,12 @@
6566
from . import config
6667
from copy import deepcopy
6768

68-
__all__ = ['StateSpace', 'tf2ss', 'ssdata']
69+
try:
70+
from slycot import ab13dd
71+
except ImportError:
72+
ab13dd = None
73+
74+
__all__ = ['StateSpace', 'tf2ss', 'ssdata', 'linfnorm']
6975

7076
# Define module default parameter values
7177
_statesp_defaults = {
@@ -1895,3 +1901,61 @@ def ssdata(sys):
18951901
"""
18961902
ss = _convert_to_statespace(sys)
18971903
return ss.A, ss.B, ss.C, ss.D
1904+
1905+
1906+
def linfnorm(sys, tol=1e-10):
1907+
"""L-infinity norm of a linear system
1908+
1909+
Parameters
1910+
----------
1911+
sys : LTI (StateSpace or TransferFunction)
1912+
system to evalute L-infinity norm of
1913+
tol : real scalar
1914+
tolerance on norm estimate
1915+
1916+
Returns
1917+
-------
1918+
gpeak : non-negative scalar
1919+
L-infinity norm
1920+
fpeak : non-negative scalar
1921+
Frequency, in rad/s, at which gpeak occurs
1922+
1923+
For stable systems, the L-infinity and H-infinity norms are equal;
1924+
for unstable systems, the H-infinity norm is infinite, while the
1925+
L-infinity norm is finite if the system has no poles on the
1926+
imaginary axis.
1927+
1928+
See also
1929+
--------
1930+
slycot.ab13dd : the Slycot routine linfnorm that does the calculation
1931+
"""
1932+
1933+
if ab13dd is None:
1934+
raise ControlSlycot("Can't find slycot module 'ab13dd'")
1935+
1936+
a, b, c, d = ssdata(_convert_to_statespace(sys))
1937+
e = np.eye(a.shape[0])
1938+
1939+
n = a.shape[0]
1940+
m = b.shape[1]
1941+
p = c.shape[0]
1942+
1943+
if n == 0:
1944+
# ab13dd doesn't accept empty A, B, C, D;
1945+
# static gain case is easy enough to compute
1946+
gpeak = scipy.linalg.svdvals(d)[0]
1947+
# max svd is constant with freq; arbitrarily choose 0 as peak
1948+
fpeak = 0
1949+
return gpeak, fpeak
1950+
1951+
dico = 'C' if sys.isctime() else 'D'
1952+
jobe = 'I'
1953+
equil = 'S'
1954+
jobd = 'Z' if all(0 == d.flat) else 'D'
1955+
1956+
gpeak, fpeak = ab13dd(dico, jobe, equil, jobd, n, m, p, a, e, b, c, d, tol)
1957+
1958+
if dico=='D':
1959+
fpeak /= sys.dt
1960+
1961+
return gpeak, fpeak

control/tests/statesp_test.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
from control.dtime import sample_system
2020
from control.lti import evalfr
2121
from control.statesp import StateSpace, _convert_to_statespace, tf2ss, \
22-
_statesp_defaults, _rss_generate
22+
_statesp_defaults, _rss_generate, linfnorm
2323
from control.iosys import ss, rss, drss
2424
from control.tests.conftest import ismatarrayout, slycotonly
2525
from control.xferfcn import TransferFunction, ss2tf
2626

27+
2728
from .conftest import editsdefaults
2829

30+
2931
class TestStateSpace:
3032
"""Tests for the StateSpace class."""
3133

@@ -1107,3 +1109,52 @@ def test_latex_repr_testsize(editsdefaults):
11071109

11081110
gstatic = ss([], [], [], 1)
11091111
assert gstatic._repr_latex_() is None
1112+
1113+
1114+
class TestLinfnorm:
1115+
# these are simple tests; we assume ab13dd is correct
1116+
# python-control specific behaviour is:
1117+
# - checking for continuous- and discrete-time
1118+
# - scaling fpeak for discrete-time
1119+
# - handling static gains
1120+
1121+
# the underdamped gpeak and fpeak are found from
1122+
# gpeak = 1/(2*zeta*(1-zeta**2)**0.5)
1123+
# fpeak = wn*(1-2*zeta**2)**0.5
1124+
@pytest.fixture(params=[
1125+
('static', ct.tf, ([1.23],[1]), 1.23, 0),
1126+
('underdamped', ct.tf, ([100],[1, 2*0.5*10, 100]), 1.1547005, 7.0710678),
1127+
])
1128+
def ct_siso(self, request):
1129+
name, systype, sysargs, refgpeak, reffpeak = request.param
1130+
return systype(*sysargs), refgpeak, reffpeak
1131+
1132+
@pytest.fixture(params=[
1133+
('underdamped', ct.tf, ([100],[1, 2*0.5*10, 100]), 1e-4, 1.1547005, 7.0710678),
1134+
])
1135+
def dt_siso(self, request):
1136+
name, systype, sysargs, dt, refgpeak, reffpeak = request.param
1137+
return ct.c2d(systype(*sysargs), dt), refgpeak, reffpeak
1138+
1139+
@slycotonly
1140+
def test_linfnorm_ct_siso(self, ct_siso):
1141+
sys, refgpeak, reffpeak = ct_siso
1142+
gpeak, fpeak = linfnorm(sys)
1143+
np.testing.assert_allclose(gpeak, refgpeak)
1144+
np.testing.assert_allclose(fpeak, reffpeak)
1145+
1146+
@slycotonly
1147+
def test_linfnorm_dt_siso(self, dt_siso):
1148+
sys, refgpeak, reffpeak = dt_siso
1149+
gpeak, fpeak = linfnorm(sys)
1150+
# c2d pole-mapping has round-off
1151+
np.testing.assert_allclose(gpeak, refgpeak)
1152+
np.testing.assert_allclose(fpeak, reffpeak)
1153+
1154+
@slycotonly
1155+
def test_linfnorm_ct_mimo(self, ct_siso):
1156+
siso, refgpeak, reffpeak = ct_siso
1157+
sys = ct.append(siso, siso)
1158+
gpeak, fpeak = linfnorm(sys)
1159+
np.testing.assert_allclose(gpeak, refgpeak)
1160+
np.testing.assert_allclose(fpeak, reffpeak)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy