@@ -611,6 +611,102 @@ def feedback(self, other=1, sign=-1):
611611
612612 return StateSpace (A , B , C , D , dt )
613613
614+ def lft (self , other , nu = - 1 , ny = - 1 ):
615+ """Return the Linear Fractional Transformation.
616+
617+ See definition here:
618+ https://www.mathworks.com/help/control/ref/lft.html
619+
620+ Parameters
621+ ----------
622+ other: LTI
623+ The lower LTI system
624+ ny: int, optional
625+ Dimension of (plant) measurement output.
626+ nu: int, optional
627+ Dimension of (plant) control input.
628+ """
629+ other = _convertToStateSpace (other )
630+ # maximal values for nu, ny
631+ if ny == - 1 :
632+ ny = min (other .inputs , self .outputs )
633+ if nu == - 1 :
634+ nu = min (other .outputs , self .inputs )
635+ # dimension check
636+ # TODO
637+
638+ # Figure out the sampling time to use
639+ if (self .dt == None and other .dt != None ):
640+ dt = other .dt # use dt from second argument
641+ elif (other .dt == None and self .dt != None ) or \
642+ timebaseEqual (self , other ):
643+ dt = self .dt # use dt from first argument
644+ else :
645+ raise ValueError ("Systems have different sampling times" )
646+
647+ # submatrices
648+ A = self .A
649+ B1 = self .B [:, :self .inputs - nu ]
650+ B2 = self .B [:, self .inputs - nu :]
651+ C1 = self .C [:self .outputs - ny , :]
652+ C2 = self .C [self .outputs - ny :, :]
653+ D11 = self .D [:self .outputs - ny , :self .inputs - nu ]
654+ D12 = self .D [:self .outputs - ny , self .inputs - nu :]
655+ D21 = self .D [self .outputs - ny :, :self .inputs - nu ]
656+ D22 = self .D [self .outputs - ny :, self .inputs - nu :]
657+
658+ # submatrices
659+ Abar = other .A
660+ Bbar1 = other .B [:, :ny ]
661+ Bbar2 = other .B [:, ny :]
662+ Cbar1 = other .C [:nu , :]
663+ Cbar2 = other .C [nu :, :]
664+ Dbar11 = other .D [:nu , :ny ]
665+ Dbar12 = other .D [:nu , ny :]
666+ Dbar21 = other .D [nu :, :ny ]
667+ Dbar22 = other .D [nu :, ny :]
668+
669+ # well-posed check
670+ F = np .block ([[np .eye (ny ), - D22 ], [- Dbar11 , np .eye (nu )]])
671+ if matrix_rank (F ) != ny + nu :
672+ raise ValueError ("lft not well-posed to working precision." )
673+
674+ # solve for the resulting ss by solving for [y, u] using [x,
675+ # xbar] and [w1, w2].
676+ TH = np .linalg .solve (F , np .block (
677+ [[C2 , np .zeros ((ny , other .states )), D21 , np .zeros ((ny , other .inputs - ny ))],
678+ [np .zeros ((nu , self .states )), Cbar1 , np .zeros ((nu , self .inputs - nu )), Dbar12 ]]
679+ ))
680+ T11 = TH [:ny , :self .states ]
681+ T12 = TH [:ny , self .states : self .states + other .states ]
682+ T21 = TH [ny :, :self .states ]
683+ T22 = TH [ny :, self .states : self .states + other .states ]
684+ H11 = TH [:ny , self .states + other .states : self .states + other .states + self .inputs - nu ]
685+ H12 = TH [:ny , self .states + other .states + self .inputs - nu :]
686+ H21 = TH [ny :, self .states + other .states : self .states + other .states + self .inputs - nu ]
687+ H22 = TH [ny :, self .states + other .states + self .inputs - nu :]
688+
689+ Ares = np .block ([
690+ [A + B2 .dot (T21 ), B2 .dot (T22 )],
691+ [Bbar1 .dot (T11 ), Abar + Bbar1 .dot (T12 )]
692+ ])
693+
694+ Bres = np .block ([
695+ [B1 + B2 .dot (H21 ), B2 .dot (H22 )],
696+ [Bbar1 .dot (H11 ), Bbar2 + Bbar1 .dot (H12 )]
697+ ])
698+
699+ Cres = np .block ([
700+ [C1 + D12 .dot (T21 ), D12 .dot (T22 )],
701+ [Dbar21 .dot (T11 ), Cbar2 + Dbar21 .dot (T12 )]
702+ ])
703+
704+ Dres = np .block ([
705+ [D11 + D12 .dot (H21 ), D12 .dot (H22 )],
706+ [Dbar21 .dot (H11 ), Dbar22 + Dbar21 .dot (H12 )]
707+ ])
708+ return StateSpace (Ares , Bres , Cres , Dres , dt )
709+
614710 def minreal (self , tol = 0.0 ):
615711 """Calculate a minimal realization, removes unobservable and
616712 uncontrollable states"""
0 commit comments