Skip to content

Commit

Permalink
Merge pull request #10 from rabraker/master
Browse files Browse the repository at this point in the history
Added wrapper for TB05AD, which finds the frequency response of a state-space system.
  • Loading branch information
roryyorke authored May 30, 2017
2 parents 5d21c69 + 05242f1 commit a618aeb
Show file tree
Hide file tree
Showing 5 changed files with 535 additions and 4 deletions.
9 changes: 5 additions & 4 deletions slycot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@
from .synthesis import sg02ad, sg03bd

# Transformation routines (9/40 wrapped)
from .transform import tb01id,tb03ad,tb04ad
from .transform import tc04ad,tc01od
from .transform import tf01md,tf01rd
from .transform import td04ad,tb01pd
from .transform import tb01id, tb03ad, tb04ad
from .transform import tb05ad
from .transform import tc04ad, tc01od
from .transform import tf01md, tf01rd
from .transform import td04ad, tb01pd

from numpy.testing import Tester
test = Tester().test
Expand Down
28 changes: 28 additions & 0 deletions slycot/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,31 @@ def tb01pd_example():
print('reduced order', out[-2])
print(out)


def tb05ad_example():
"""
Example of calculating the frequency response using tb05ad
on a second-order system with a natural frequency of 10 rad/s
and damping ratio of 1.05.
"""
import numpy as np
A = np.array([[0.0, 1.0],
[-100.0, -20.1]])
B = np.array([[0.],[100]])
C = np.array([[1., 0.]])
n = np.shape(A)[0]
m = np.shape(B)[1]
p = np.shape(C)[0]

jw_s = [1j*11, 1j*15]
at, bt, ct, g_1, hinvb,info = slycot.tb05ad(n, m, p, jw_s[0],
A, B, C, job='NG')
g_2, hinv2, info = slycot.tb05ad(n, m, p, jw_s[1], at, bt, ct, job='NH')
print('--- Example for tb05ad...')
print('Frequency response for (A, B, C)')
print('-------------------------')
print('Frequency | Response')
print('%s | %s '%(jw_s[0], g_1[0, 0]))
print('%s | %s '%(jw_s[1], g_2[0, 0]))


94 changes: 94 additions & 0 deletions slycot/src/transform.pyf
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,100 @@ subroutine tb04ad_c(rowcol,n,m,p,a,lda,b,ldb,c,ldc,d,ldd,nr,index_bn,dcoeff,lddc
integer optional :: ldwork = max(1,n*(n+1)+max(n*p+2*n+max(n,p),max(3*p,m)))
integer intent(out) :: info
end subroutine tb04ad_c
subroutine tb05ad_ag(baleig,inita,n,m,p,freq,a,lda,b,ldb,c,ldc,rcond,g,ldg,evre,evim,hinvb,ldhinv,iwork,dwork,ldwork,zwork,lzwork,info)
! The case where A is is a general matrix that should be balanced
! and converted to upper Hessenberg form.
fortranname tb05ad
character intent(hide) :: baleig = 'A'
character intent(hide) :: inita = 'G'
integer check(n>0) :: n
integer check(m>0) :: m
integer check(p>0) :: p
complex*16 intent(in) :: freq
double precision intent(in,out,copy),dimension(n,n),depend(n) :: a
integer intent(hide),depend(a) :: lda=shape(a,0)
double precision intent(in,out,copy),dimension(n,m),depend(n,m) :: b
integer intent(hide),depend(b) :: ldb=shape(b,0)
double precision intent(in,out,copy),dimension(p,n),depend(n,p) :: c
integer intent(hide),depend(c) :: ldc=shape(c,0)
double precision intent(out) :: rcond
complex*16 intent(out),dimension(ldg,m),depend(ldg,m) :: g
integer intent(hide),depend(p) :: ldg = p
double precision intent(out),dimension(n),depend(n):: evre
double precision intent(out),dimension(n),depend(n):: evim
complex*16 intent(out),dimension(ldhinv,m),depend(ldhinv,m) :: hinvb
integer intent(hide),depend(n) :: ldhinv = n
! cache variables
integer intent(hide,cache),dimension(n) :: iwork
double precision intent(hide,cache),dimension(ldwork),depend(ldwork) :: dwork
integer optional,depend(n) :: ldwork = 2*n
complex*16 intent(hide,cache),dimension(lzwork),depend(lzwork) :: zwork
integer optional,depend(n) :: lzwork = n*n+2*n
integer intent(out) :: info
end subroutine tb05ad_ag
subroutine tb05ad_ng(baleig,inita,n,m,p,freq,a,lda,b,ldb,c,ldc,rcond,g,ldg,evre,evim,hinvb,ldhinv,iwork,dwork,ldwork,zwork,lzwork,info)
! The case where A is is a general matrix that should not be
! balanced but is only converted to upper Hessenberg form.
fortranname tb05ad
character intent(hide) :: baleig = 'N'
character intent(hide) :: inita = 'G'
integer check(n>0) :: n
integer check(m>0) :: m
integer check(p>0) :: p
complex*16 intent(in) :: freq
double precision intent(in,out,copy),dimension(n,n),depend(n) :: a
integer intent(hide),depend(a) :: lda=shape(a,0)
double precision intent(in,out,copy),dimension(n,m),depend(n,m) :: b
integer intent(hide),depend(b) :: ldb=shape(b,0)
double precision intent(in,out,copy),dimension(p,n),depend(n,p) :: c
integer intent(hide),depend(c) :: ldc=shape(c,0)
double precision intent(hide) :: rcond
complex*16 intent(out),dimension(ldg,m),depend(ldg,m) :: g
integer intent(hide),depend(p) :: ldg = p
double precision intent(hide),dimension(n),depend(n):: evre
double precision intent(hide),dimension(n),depend(n):: evim
complex*16 intent(out),dimension(ldhinv,m),depend(ldhinv,m) :: hinvb
integer intent(hide),depend(n) :: ldhinv = n
! cache variables
integer intent(hide,cache),dimension(n) :: iwork
double precision intent(hide,cache),dimension(ldwork),depend(ldwork) :: dwork
integer optional,depend(n) :: ldwork = 2*n
complex*16 intent(hide,cache),dimension(lzwork),depend(lzwork) :: zwork
integer optional,depend(n) :: lzwork = n*n+2*n
integer intent(out) :: info
end subroutine tb05ad_ng

subroutine tb05ad_nh(baleig,inita,n,m,p,freq,a,lda,b,ldb,c,ldc,rcond,g,ldg,evre,evim,hinvb,ldhinv,iwork,dwork,ldwork,zwork,lzwork,info)
! The case where A is already balanced and hessenberg
fortranname tb05ad
character intent(hide) :: baleig = 'N'
character intent(hide) :: inita = 'H'
integer check(n>0) :: n
integer check(m>0) :: m
integer check(p>0) :: p
complex*16 intent(in) :: freq
double precision intent(in),dimension(n,n),depend(n) :: a
integer intent(hide),depend(a) :: lda=shape(a,0)
double precision intent(in),dimension(n,m),depend(n,m) :: b
integer intent(hide),depend(b) :: ldb=shape(b,0)
double precision intent(in),dimension(p,n),depend(n,p) :: c
integer intent(hide),depend(c) :: ldc=shape(c,0)
double precision intent(hide) :: rcond
complex*16 intent(out),dimension(ldg,m),depend(ldg,m) :: g
integer intent(hide),depend(p) :: ldg = p
double precision intent(hide),dimension(n),depend(n):: evre
double precision intent(hide),dimension(n),depend(n):: evim
complex*16 intent(out),dimension(ldhinv,m),depend(ldhinv,m) :: hinvb
integer intent(hide),depend(n) :: ldhinv = n
! cache variables
integer intent(hide,cache),dimension(n) :: iwork
double precision intent(hide,cache),dimension(ldwork),depend(ldwork) :: dwork
integer optional,depend(n) :: ldwork = 2*n
complex*16 intent(hide,cache),dimension(lzwork),depend(lzwork) :: zwork
integer optional,depend(n) :: lzwork = n*n+2*n
integer intent(out) :: info
end subroutine tb05ad_h

subroutine tc01od_l(leri,m,p,indlim,pcoeff,ldpco1,ldpco2,qcoeff,ldqco1,ldqco2,info) ! in TC01OD.f
fortranname tc01od
character intent(hide) :: leri = 'L'
Expand Down
165 changes: 165 additions & 0 deletions slycot/tests/test_tb05ad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# ===================================================
# tb05ad tests
import unittest
from slycot import transform
import numpy as np

from numpy.testing import assert_raises, assert_almost_equal


# set the random seed so we can get consistent results.
np.random.seed(40)
CASES = {}

# This is a known failure for tb05ad when running job 'AG'
CASES['fail1'] = {'A': np.array([[-0.5, 0., 0., 0. ],
[ 0., -1., 0. , 0. ],
[ 1., 0., -0.5, 0. ],
[ 0., 1., 0., -1. ]]),
'B': np.array([[ 1., 0.],
[ 0., 1.],
[ 0., 0.],
[ 0., 0.]]),
'C': np.array([[ 0., 1., 1., 0.],
[ 0., 1., 0., 1.],
[ 0., 1., 1., 1.]])}

n = 20
p = 10
m = 14

CASES['pass1'] = {'A': np.random.randn(n, n),
'B': np.random.randn(n, m),
'C': np.random.randn(p, n)}


class test_tb05ad(unittest.TestCase):

def test_tb05ad_ng(self):
"""
Test that tb05ad with job 'NG' computes the correct
frequency response.
"""
for key in CASES:
sys = CASES[key]
self.check_tb05ad_AG_NG(sys, 10*1j, 'NG')

@unittest.expectedFailure
def test_tb05ad_ag_failure(self):
""" Test tb05ad and job 'AG' (i.e., balancing enabled) fails
on certain A matrices.
"""
self.check_tb05ad_AG_NG(CASES['fail1'], 10*1j, 'AG')

def test_tb05ad_nh(self):
"""Test that tb05ad with job = 'NH' computes the correct
frequency response after conversion to Hessenberg form.
First call tb05ad with job='NH' to transform to upper Hessenberg
form which outputs the transformed system.
Subsequently, call tb05ad with job='NH' using this transformed system.
"""
jomega = 10*1j
for key in CASES:
sys = CASES[key]
sys_transformed = self.check_tb05ad_AG_NG(sys, jomega, 'NG')
self.check_tb05ad_NH(sys_transformed, sys, jomega)


def test_tb05ad_errors(self):
"""
Test tb05ad error handling. We give wrong inputs and
and check that this raises an error.
"""
self.check_tb05ad_errors(CASES['pass1'])

def check_tb05ad_AG_NG(self, sys, jomega, job):
"""
Check that tb05ad computes the correct frequency response when
running jobs 'AG' and/or 'NG'.
Inputs
------
sys: A a dict of system matrices with keys 'A', 'B', and 'C'.
jomega: A complex scalar, which is the frequency we are
evaluating the system at.
job: A string, either 'AG' or 'NH'
Returns
-------
sys_transformed: A dict of the system matrices which have been
transformed according the job.
"""
n, m = sys['B'].shape
p = sys['C'].shape[0]
result = transform.tb05ad(n, m, p, jomega,
sys['A'], sys['B'], sys['C'], job=job)
g_i = result[3]
hinvb = np.linalg.solve(np.eye(n) * jomega - sys['A'], sys['B'])
g_i_solve = sys['C'].dot(hinvb)
assert_almost_equal(g_i_solve, g_i)
sys_transformed = {'A': result[0], 'B': result[1], 'C': result[2]}
return sys_transformed

def check_tb05ad_NH(self, sys_transformed, sys, jomega):
"""
Check tb05ad, computes the correct frequency response when
job='NH' and we supply system matrices 'A', 'B', and 'C'
which have been transformed by a previous call to tb05ad.
We check we get the same result as computing C(sI - A)^-1B
with the original system.
Inputs
------
sys_transformed: A a dict of the transformed (A in upper
hessenberg form) system matrices with keys
'A', 'B', and 'C'.
sys: A dict of the original un-transformed system matrices.
jomega: A complex scalar, which is the frequency to evaluate at.
"""

n, m = sys_transformed['B'].shape
p = sys_transformed['C'].shape[0]
result = transform.tb05ad(n, m, p, jomega, sys_transformed['A'],
sys_transformed['B'], sys_transformed['C'],
job='NH')
g_i = result[0]
hinvb = np.linalg.solve(np.eye(n) * jomega - sys['A'], sys['B'])
g_i_solve = sys['C'].dot(hinvb)
assert_almost_equal(g_i_solve, g_i)

def check_tb05ad_errors(self, sys):
"""
Check the error handling of tb05ad. We give wrong inputs and
and check that this raises an error.
"""
n, m = sys['B'].shape
p = sys['C'].shape[0]
jomega = 10*1j
# test error handling
# wrong size A
assert_raises(ValueError, transform.tb05ad, n+1, m, p,
jomega, sys['A'], sys['B'], sys['C'], job='NH')
# wrong size B
assert_raises(ValueError, transform.tb05ad, n, m+1, p,
jomega, sys['A'], sys['B'], sys['C'], job='NH')
# wrong size C
assert_raises(ValueError, transform.tb05ad, n, m, p+1,
jomega, sys['A'], sys['B'], sys['C'], job='NH')
# unrecognized job
assert_raises(ValueError, transform.tb05ad, n, m, p, jomega,
sys['A'], sys['B'], sys['C'], job='a')



def suite():
return unittest.TestLoader().loadTestsFromTestCase(TestConvert)


if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit a618aeb

Please sign in to comment.