Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Elegant way to generate a transfer function matrix #325

Closed
iljastas opened this issue Jul 10, 2019 · 18 comments
Closed

Elegant way to generate a transfer function matrix #325

iljastas opened this issue Jul 10, 2019 · 18 comments

Comments

@iljastas
Copy link

iljastas commented Jul 10, 2019

Hi everybody,
I want to know if there is a more elegant way to generate a transfer-function-matrix for a hinf-controller.

In matlab I just do:
P11 = [0 0; W2G W2]
P12 = [W1; W2
G]
P21 = [G 1]
P22 = [G]
P_ = [P11 P12; P21 P22]
sys = ss(P_, 'minimal');

In python with control, I habe to do:
import control
from control import *
Gss = ss([[-1.0, -10.0], [1, 0]], [[10.0], [0]], [0, 1], [0])
G = tf(Gss)
W1 = tf([100], [0.1, 1])
W2 = tf([100], [0.1, 1])
W2G = W2*G
num = [ [ [0], [0], W1.num[0][0] ], [W2G.num[0][0], W2.num[0][0], G.num[0][0]], [G.num[0][0], [1], G.num[0][0] ] ]
den = [ [ [1], [1], W1.den[0][0] ], [W2G.den[0][0], W2.den[0][0], G.den[0][0]], [G.den[0][0], [1], G.den[0][0] ] ]
P = tf( num, den )
print(P)

The problem is that the return of the numerator and denominator is not directly the list.

And I get a high order system. Should I use just the balanced trunction?
Pss = ss(P)
Pbr = balred(Pss, 4)


Matlab is using minreal with exist also in "control", but I have to find manually a suitable tolerance...

@murrayrm
Copy link
Member

You could try using parallel, series and feedback to construct the system. Alternatively, take a look at the append and connect commands? All of those should produce a system that doesn't involve creating extra states (unless you list one of the systems multiple times, which I guess is what is causing the problem above?).

@murrayrm
Copy link
Member

murrayrm commented Jul 13, 2019

The following should work:

# Create a 3 input, 3 output system consisting of component systems
sysvec = append(ss(W1), ss(W2), Gss)

# Add interconnections between the components (NB: indexing starts at 1)
# Input of G (3) is output of W1 (1), input of W2 (2) is G (3)
# Keep all inputs and outputs
P_ = connect(sysvec, [[3, 1], [2, 3]], [1, 2, 3], [1, 2, 3])

Note that this generates a minimal representation in the state space because we never create a copy of the plant (G) or weighting filters. So P_ has 4 states, 3 inputs, 3 outputs.

One annoying (and I think confusing) aspect of this construction is that the control.connect function uses indexing starting at 1 (to be compatible with MATLAB but also to allow negative feedback), while Python indexing normally starts at 0. This is documented in the connect function, but I still think it is confusing.

@iljastas
Copy link
Author

Thanks for your help @murrayrm !
But I don't get it. Input of G is the controller output or better say the disturbed controller output.

@murrayrm
Copy link
Member

I may have misunderstood the transfer function matrix you were trying to create (I sketched it out on a napkin that I have since tossed -:). The main point is that you can use append to stack the system dynamics and then use connect to interconnect them.

@iljastas
Copy link
Author

iljastas commented Jul 16, 2019

I'm unfortunately not able to produce a minimal example (Kai Müller, Entwurf robubster Regelungen Teubner Springer 1996, Chapter 12.3.1, p. 172).

ttttt

from control import *
W1 = tf([2], [1])
W2 = tf([3], [1])
G = tf([1], [1, 1])
sysvec = append(ss(G), ss(W1), ss(W2))
P = connect(sysvec, [[1, 2] ], [2, 1], [2, 3, 1])
print(P)

A = [[-1.]]
B = [[1. 0.]]
C = [[1.]
[0.]
[ 0.]]
D = [[0. 0.]
[0. 2.]
[0. 0.]]

The right answer is: A=[-1], B = [0 1], C=[2 0 1]^T, D=[2 0; 0 3; 1 0]
But I'm not able to produce it.

So there is a connection between G and W1. And I have to inputs: u and z, And I have three outputs: v1, v2 and y.

@murrayrm
Copy link
Member

murrayrm commented Jul 16, 2019

I put this example into MATLAB (same exact commands) and got the output

P =
 
  A = 
       x1
   x1  -1
 
  B = 
       u1  u2
   x1   2   1
 
  C = 
       x1
   y1   0
   y2   0
   y3   1
 
  D = 
       u1  u2
   y1   2   0
   y2   0   0
   y3   0   0

When I run those commands in Python 3.5 using the current master (which should be the same as v0.8.2), I get output

A = [[-1.]]

B = [[2. 1.]]

C = [[0.]
 [0.]
 [1.]]

D = [[2. 0.]
 [0. 0.]
 [0. 0.]]

This matches the output of MATLAB, but is different than what you showed as your output. What version of python-control are you running?

There is still the issue that this is not the right system description for the block diagram. I think that part of the problem is that your interconnection matrix is not right. You have [[1, 2]] for the interconnection, which sets input 1 (G) to output 2 (W1). From the block diagram, what you want is [[2, 1]], so that the input of W1 (= input #2) is the output from G (output #1).

Running the command with the updated interconnection matrix gives

P = connect(sysvec, [[2, 1]], [2, 1], [2, 3, 1])

gives

A = [[-1.]]

B = [[0. 1.]]

C = [[2.]
 [0.]
 [1.]]

D = [[2. 0.]
 [0. 0.]
 [0. 0.]]

This is almost what you expect, except that the D matrix is not correct.

The problem is because of a limitations in the connect command (in MATLAB and Python):

  • The outputs for the connect command can only correspond to outputs from one of the subsystems. The output that you want for y in your diagram is the output of G plus the input z. But you can't actually do that with connect (in MATLAB or Python). So one problem is that the input u should be going to output v2 through a gain of 3. (That entry is missing in both the MATLAB and Python versions, for reasons that don't make senses.)

  • There is a similar constrain on the inputs. It is not possible to take an input from one system and map it to the input of another. So the only input to W2 is its natural input and we removed this from the transfer function.

@bnavigator
Copy link
Contributor

bnavigator commented Jul 16, 2019

Try this:

from control import tf,ss,connect,append

UU = tf([1],[1])     #1 u   ->u_o
Z  = tf([1],[1])     #2 z   ->z_o
G  = tf([1], [1, 1]) #3 ug  ->y 
W1 = tf([2], [1])    #4 uw1  ->v1
W2 = tf([3], [1])    #5 uw2  ->v2
ZY = tf([1],[1])     #6 zy_i->zy_o

sysvec = append(ss(UU),ss(Z),ss(G),ss(W1),ss(W2),ss(ZY))
Q=[[6,2],[6,3], # zy_i=z_o+y
   [4,6],       # uw1=zy_o
   [3,1],       # ug = u_o
   [5,1],       # uw2 =u_o
   ]
P = connect(sysvec,Q,[2,1],[4,5,6]) # z,u -> v1,v2,z+y,
print(P)

which gives your "right answer"

A = [[-1.]]

B = [[0. 1.]]

C = [[2.]
 [0.]
 [1.]]

D = [[2. 0.]
 [0. 3.]
 [1. 0.]]

I am pretty sure your D does not reflect your desired output of (v1,v2,y) but it is (v1,v2,z+y) and your inputs are ordered (z,u)

@murrayrm
Copy link
Member

Thanks @bnavigator. It would be useful to think about whether there is a nice way to do these sorts of manipulations. The construct that @iljastas gave at the start is really the best way since it creates copies of the system/weighting filters that are then "minimized" away, but it is also tedious to put in the extra elements that you used to get things to work.

I'd lover to hear suggestions about what a cleaner implementation would look like.

@iljastas
Copy link
Author

Thanks @murrayrm and @bnavigator I will try it later or tomorrow and post also a more complex example with and hinfsyn-controller!

@bnavigator
Copy link
Contributor

Maybe it would be useful to have a function combine() or something similar to mimic the Matrix combination of systems like in MATLAB. Here is a hack how that could look like. Of course it still needs all the type checks etc. to have it included into the StateSpace class:

import numpy as np
from control import StateSpace,tf,minreal
from control.statesp import _convertToStateSpace

def combine(systems):
    """ systems: 2D array of systems to combine """
    
    rrows=[]
    for srow in systems:
        s1 = srow[0]
        if not isinstance(s1,StateSpace):
            s1=_convertToStateSpace(s1)            
            
        for s2 in srow[1:]:
            if not isinstance(s2, StateSpace):
                s2 = _convertToStateSpace(s2)
            if s1.dt != s2.dt:
                raise ValueError("Systems must have the same time step")            
            n = s1.states + s2.states
            m = s1.inputs + s2.inputs
            p = s1.outputs
            if s2.outputs != p:
                raise ValueError('inconsistent systems')
            A = np.zeros((n, n))
            B = np.zeros((n, m))
            C = np.zeros((p, n))
            D = np.zeros((p, m))
            A[:s1.states, :s1.states] = s1.A
            A[s1.states:, s1.states:] = s2.A
            B[:s1.states, :s1.inputs] = s1.B
            B[s1.states:, s1.inputs:] = s2.B
            C[:, :s1.states] = s1.C
            C[:, s1.states:] = s2.C
            D[:, :s1.inputs] = s1.D
            D[:, s1.inputs:] = s2.D
            s1=StateSpace(A,B,C,D,s1.dt)
        rrows.append(s1)
    r1=rrows[0]
    for r2 in rrows[1:]:
        if r1.dt != r2.dt:
            raise ValueError("Systems must have the same time step")            
        n = r1.states + r2.states
        m = r1.inputs
        if r2.inputs != m:
            raise ValueError('inconsistent systems')
        p = r1.outputs + r2.outputs
        A = np.zeros((n, n))
        B = np.zeros((n, m))
        C = np.zeros((p, n))
        D = np.zeros((p, m))
        A[:r1.states, :r1.states] = r1.A
        A[r1.states:, r1.states:] = r2.A
        B[:r1.states, :] = r1.B
        B[r1.states:, :] = r2.B
        C[:r1.outputs, :r1.states] = r1.C
        C[r1.outputs:, r1.states:] = r2.C
        D[:r1.outputs, :] = r1.D
        D[r1.outputs:, :] = r2.D
        r1=StateSpace(A,B,C,D,r1.dt)
    return r1

Gss = StateSpace([[-1.0, -10.0], [1, 0]], [[10.0], [0]], [0, 1], [0])
G = tf(Gss)
W1 = tf([100], [0.1, 1])
W2 = tf([100], [0.1, 1])
W2G=W2*G   
P11=np.block([[0,0],[W2G,W2]])
P12=np.block([[W1],[W2G]])
P21=np.block([[G,1]])
P22=np.block([[G]])
P_=np.block([[P11,P12],[P21,P22]])
Pc=combine(P_)
Pctf=minreal(tf(Pc))

output:

8 states have been removed from the model

Input 1 to output 1:
0
-
1

Input 1 to output 2:
          1e+04
-------------------------
s^3 + 11 s^2 + 20 s + 100

Input 1 to output 3:
     10
------------
s^2 + s + 10

Input 2 to output 1:
0
-
1

Input 2 to output 2:
 1000
------
s + 10

Input 2 to output 3:
1
-
1

Input 3 to output 1:
 1000
------
s + 10

Input 3 to output 2:
   1.756e-12 s + 1e+04
-------------------------
s^3 + 11 s^2 + 20 s + 100

Input 3 to output 3:
     10
------------
s^2 + s + 10

MATLAB:

>> G=ss([-1,-10;1,0],[10;0],[0,1],[0]);
>> W1=tf(100,[0.1 1]);
>> W2=W1;
>> W2G=W2*G;
>> P11 = [0 0; W2G W2];
>> P12 = [W1; W2G];
>> P21 = [G 1];
>> P22 = [G];
>> Pc = [P11 P12; P21 P22];
>> Pctf=tf(Pc)

Pctf =
 
  From input 1 to output...
   1:  0
 
                 10000
   2:  -------------------------
       s^3 + 11 s^2 + 20 s + 100
 
            10
   3:  ------------
       s^2 + s + 10
 
  From input 2 to output...
   1:  0
 
        1000
   2:  ------
       s + 10
 
   3:  1
 
  From input 3 to output...
        1000
   1:  ------
       s + 10
 
                 10000
   2:  -------------------------
       s^3 + 11 s^2 + 20 s + 100
 
            10
   3:  ------------
       s^2 + s + 10
 
Continuous-time transfer function.

A combine() for TransferFunction would be even simpler, but that is just what has been done manually in the ininital question:

In [113]: from control import *
     ...: Gss = ss([[-1.0, -10.0], [1, 0]], [[10.0], [0]], [0, 1], [0])
     ...: G = tf(Gss)
     ...: W1 = tf([100], [0.1, 1])
     ...: W2 = tf([100], [0.1, 1])
     ...: W2G = W2*G
     ...: num = [ [ [0], [0], W1.num[0][0] ], [W2G.num[0][0], W2.num[0][0], G.num[0][0]], [G.num[0][0], [1], G.num[0][0] ] ]
     ...: den = [ [ [1], [1], W1.den[0][0] ], [W2G.den[0][0], W2.den[0][0], G.den[0][0]], [G.den[0][0], [1], G.den[0][0] ] ]
     ...: P = tf( num, den )
     ...: print(minreal(P))
-2 states have been removed from the model

Input 1 to output 1:
0
-
1

Input 1 to output 2:
          1e+04
-------------------------
s^3 + 11 s^2 + 20 s + 100

Input 1 to output 3:
     10
------------
s^2 + s + 10

Input 2 to output 1:
0
-
1

Input 2 to output 2:
 1000
------
s + 10

Input 2 to output 3:
1
-
1

Input 3 to output 1:
 1000
------
s + 10

Input 3 to output 2:
     10
------------
s^2 + s + 10

Input 3 to output 3:
     10
------------
s^2 + s + 10

@iljastas
Copy link
Author

iljastas commented Jul 17, 2019

I was not able to get the plant in a more complex example by the append, connect procedure.
Because I have a MIMO-System (G) with a disturance z2 after G: z2, G element of R^2 and W1 has to 2 inputs and two outputs

But thanks to @bnavigator: the combine-method is great. Thanks a lot! Maybe my example will help others.

image

import control
from control import *
import matplotlib.pyplot as plt
import numpy as np
from control.statesp import _convertToStateSpace
def combine(systems):

       rrows=[]
    for srow in systems:
        s1 = srow[0]
        if not isinstance(s1,StateSpace):
            s1=_convertToStateSpace(s1)            
            
        for s2 in srow[1:]:
            if not isinstance(s2, StateSpace):
                s2 = _convertToStateSpace(s2)
            if s1.dt != s2.dt:
                raise ValueError("Systems must have the same time step")            
            n = s1.states + s2.states
            m = s1.inputs + s2.inputs
            p = s1.outputs
            if s2.outputs != p:
                raise ValueError('inconsistent systems')
            A = np.zeros((n, n))
            B = np.zeros((n, m))
            C = np.zeros((p, n))
            D = np.zeros((p, m))
            A[:s1.states, :s1.states] = s1.A
            A[s1.states:, s1.states:] = s2.A
            B[:s1.states, :s1.inputs] = s1.B
            B[s1.states:, s1.inputs:] = s2.B
            C[:, :s1.states] = s1.C
            C[:, s1.states:] = s2.C
            D[:, :s1.inputs] = s1.D
            D[:, s1.inputs:] = s2.D
            s1=StateSpace(A,B,C,D,s1.dt)
        rrows.append(s1)
    r1=rrows[0]
    for r2 in rrows[1:]:
        if r1.dt != r2.dt:
            raise ValueError("Systems must have the same time step")            
        n = r1.states + r2.states
        m = r1.inputs
        if r2.inputs != m:
            raise ValueError('inconsistent systems')
        p = r1.outputs + r2.outputs
        A = np.zeros((n, n))
        B = np.zeros((n, m))
        C = np.zeros((p, n))
        D = np.zeros((p, m))
        A[:r1.states, :r1.states] = r1.A
        A[r1.states:, r1.states:] = r2.A
        B[:r1.states, :] = r1.B
        B[r1.states:, :] = r2.B
        C[:r1.outputs, :r1.states] = r1.C
        C[r1.outputs:, r1.states:] = r2.C
        D[:r1.outputs, :] = r1.D
        D[r1.outputs:, :] = r2.D
        r1=StateSpace(A,B,C,D,r1.dt)
    return r1
def make_lowpass(dc, crossw):
    return tf([dc], [crossw, 1])
def make_highpass(hf_gain, m3db_frq):
    return tf([hf_gain, 0], [1, m3db_frq])
def make_weight(dc, crossw, hf):
    s = tf([1, 0], [1])
    return (s * hf + crossw) / (s + crossw / dc)
v = 1.1    
lo = 2.2
A = [ [0, v], [0, 0] ]
B = [ [lo], [1] ]
C = [ [1, 0], [0, 1] ] 
D = [ [0], [0]]
Gss = ss(A, B, C, D)
G = tf(Gss)       
G1 = G[0,0]
G2 = G[1,0]


Wy1 = make_weight(dc=1, crossw=2.1, hf=0)
Wy2 = make_highpass(hf_gain=0.99, m3db_frq=2.1)
Wu = make_highpass(hf_gain=0.3, m3db_frq=2.1)

# P11 = [ [W1, 0, W1*G1], [0, W2, W2*G2], [0, 0, 0] ]
# P12 = [ [W1*G1], [W2*G2], [Wu] ]
# P21 = [ [-1, 0, -G1], [0, -1, -G2] ]
# P22 = [ [-G1], [-G2] ]
# P = [ [P11, P21], [P21, P22]]

P11=np.block( [ [Wy1, 0, Wy1*G1], [0, Wy2, Wy2*G2], [0, 0,0] ] ) 
P12=np.block( [ [Wy1*G1], [Wy2*G2], [Wu] ])
P21=np.block( [ [-1, 0, -G1], [0, -1, -G2] ] )
P22=np.block( [ [-G1], [-G2] ] )
P_ = np.block( [ [P11, P12], [P21, P22]] )
Pc = combine(P_)
print(Pc.A.shape)
Pcss = minreal( Pc ) 
print(Pcss)

K, CL, gam, rcond = hinfsyn(Pcss, 2, 1)

@bnavigator
Copy link
Contributor

Please format your posts to include code within code tags. Mark the multiline code and click on the '<>' button. It will enclose the code in ``` tags

```
def yourcode():
    pass
```

results in

def yourcode():
    pass

with the python tag you even get some highlighting:

```python
def yourcode():
    pass
```
def yourcode():
    pass

See also https://guides.github.com/features/mastering-markdown/

@bnavigator
Copy link
Contributor

Much better now :)

BTW have you looked at control.robust.augw()? It does something similar using the connect() function and intermediate "distribution" and "summer" systems Ieand Iu.

@iljastas
Copy link
Author

Of course I tried this with "control" and in MATLAB but I got the errror:

ValueError: The matrix [A-jomegaI, B1 ; C2, D21] does not have full row rank with respect to the tolerance eps.

for:

from control import *

def make_weight(dc, crossw, hf):
    s = tf([1, 0], [1])
    return (s * hf + crossw) / (s + crossw / dc)

def merge_tfs(tf1, tf2):
    num = [ [tf1.num[0][0], [0] ], [ [0], tf2.num[0][0]] ]   
    den = [ [tf1.den[0][0], [1] ], [ [1], tf2.den[0][0]] ]   
    return tf( num, den ) 

v, lo = 1.1, 1.4
A = [ [0, v], [0, 0] ]
B = [ [lo], [1] ]
C = [ [1, 0], [0, 1] ] 
D = [ [0], [0] ]
Gss = ss(A, B, C, D)

w1y = make_weight(dc=100.0, crossw=0.1, hf=0)
w1Y = make_weight(dc=0.9, crossw=0.5, hf=0)
w1 =  merge_tfs(w1y, w1Y)
w2 = make_weight(dc=0.1, crossw=0.8, hf=1.0)

print("self.Gss:\n", Gss)
Paug = augw(g=Gss, w1=ss(w1) )
print(Paug)
Kss, CL, gamma, rcond = hinfsyn(Paug, 1, 1)
print("hinfsyn gamma:", gamma)

And further on my control system has only to control disturbances, so that this formulation is more suited to my problem

@ilayn
Copy link

ilayn commented Jul 19, 2019

In matlab, use lmi method to avoid the rank conditions but still always penalize your control output even the slightest to avoid the ill-conditioned solutions.

@iljastas
Copy link
Author

@ilyan do you have an example or a literature source for solving this as a lmi formulation?

@ilayn
Copy link

ilayn commented Jul 20, 2019

@iljastas Just have a look at the hinfsyn options. You can change the method. If nothing has changed in the last years, the default should be riccati based but you can change it to lmi. See this page https://nl.mathworks.com/help/robust/ref/hinfsynoptions.html

@murrayrm
Copy link
Member

This issue has been idle for ~6 months and I don't think there is anything actionable in the comments except some ideas for future functionality. Closing for now, but someone should create a new issue if there is something specific that we would like to request as an enhancement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants