4646from .mateqn import care , dare , _check_shape
4747from .statesp import StateSpace , _ssmatrix , _convert_to_statespace
4848from .lti import LTI , isdtime , isctime
49+ from .iosys import InputOutputSystem , NonlinearIOSystem , LinearIOSystem , \
50+ interconnect , ss
4951from .exception import ControlSlycot , ControlArgument , ControlDimension , \
5052 ControlNotImplemented
5153
@@ -69,7 +71,7 @@ def sb03md(n, C, A, U, dico, job='X',fact='N',trana='N',ldwork=None):
6971
7072
7173__all__ = ['ctrb' , 'obsv' , 'gram' , 'place' , 'place_varga' , 'lqr' , 'lqe' ,
72- 'dlqr' , 'dlqe' , 'acker' ]
74+ 'dlqr' , 'dlqe' , 'acker' , 'create_statefbk_iosystem' ]
7375
7476
7577# Pole placement
@@ -785,6 +787,193 @@ def dlqr(*args, **keywords):
785787 return _ssmatrix (K ), _ssmatrix (S ), E
786788
787789
790+ # Function to create an I/O sytems representing a state feedback controller
791+ def create_statefbk_iosystem (
792+ sys , K , integral_action = None , xd_labels = 'xd[{i}]' , ud_labels = 'ud[{i}]' ,
793+ estimator = None , type = 'linear' ):
794+ """Create an I/O system using a (full) state feedback controller
795+
796+ This function creates an input/output system that implements a
797+ state feedback controller of the form
798+
799+ u = ud - K_p (x - xd) - K_i integral(C x - C x_d)
800+
801+ It can be called in the form
802+
803+ ctrl, clsys = ct.create_statefbk_iosystem(sys, K)
804+
805+ where ``sys`` is the process dynamics and ``K`` is the state (+ integral)
806+ feedback gain (eg, from LQR). The function returns the controller
807+ ``ctrl`` and the closed loop systems ``clsys``, both as I/O systems.
808+
809+ Parameters
810+ ----------
811+ sys : InputOutputSystem
812+ The I/O system that represents the process dynamics. If no estimator
813+ is given, the output of this system should represent the full state.
814+
815+ K : ndarray
816+ The state feedback gain. This matrix defines the gains to be
817+ applied to the system. If ``integral_action`` is None, then the
818+ dimensions of this array should be (sys.ninputs, sys.nstates). If
819+ `integral action` is set to a matrix or a function, then additional
820+ columns represent the gains of the integral states of the
821+ controller.
822+
823+ xd_labels, ud_labels : str or list of str, optional
824+ Set the name of the signals to use for the desired state and inputs.
825+ If a single string is specified, it should be a format string using
826+ the variable ``i`` as an index. Otherwise, a list of strings matching
827+ the size of xd and ud, respectively, should be used. Default is
828+ ``'xd[{i}]'`` for xd_labels and ``'xd[{i}]'`` for ud_labels.
829+
830+ integral_action : None, ndarray, or func, optional
831+ If this keyword is specified, the controller can include integral
832+ action in addition to state feedback. If ``integral_action`` is an
833+ ndarray, it will be multiplied by the current and desired state to
834+ generate the error for the internal integrator states of the control
835+ law. If ``integral_action`` is a function ``h``, that function will
836+ be called with the signature h(t, x, u, params) to obtain the
837+ outputs that should be integrated. The number of outputs that are
838+ to be integrated must match the number of additional columns in the
839+ ``K`` matrix.
840+
841+ estimator : InputOutputSystem, optional
842+ If an estimator is provided, using the states of the estimator as
843+ the system inputs for the controller.
844+
845+ type : 'nonlinear' or 'linear', optional
846+ Set the type of controller to create. The default is a linear
847+ controller implementing the LQR regulator. If the type is 'nonlinear',
848+ a :class:NonlinearIOSystem is created instead, with the gain ``K`` as
849+ a parameter (allowing modifications of the gain at runtime).
850+
851+ Returns
852+ -------
853+ ctrl : InputOutputSystem
854+ Input/output system representing the controller. This system takes
855+ as inputs the desired state xd, the desired input ud, and the system
856+ state x. It outputs the controller action u according to the
857+ formula u = ud - K(x - xd). If the keyword `integral_action` is
858+ specified, then an additional set of integrators is included in the
859+ control system (with the gain matrix K having the integral gains
860+ appended after the state gains).
861+
862+ clsys : InputOutputSystem
863+ Input/output system representing the closed loop system. This
864+ systems takes as inputs the desired trajectory (xd, ud) and outputs
865+ the system state x and the applied input u (vertically stacked).
866+
867+ """
868+ # Make sure that we were passed an I/O system as an input
869+ if not isinstance (sys , InputOutputSystem ):
870+ raise ControlArgument ("Input system must be I/O system" )
871+
872+ # See whether we were give an estimator
873+ if estimator is not None :
874+ # Check to make sure the estimator is the right size
875+ if estimator .noutputs != sys .nstates :
876+ raise ControlArgument ("Estimator output size must match state" )
877+ elif sys .noutputs != sys .nstates :
878+ # If no estimator, make sure that the system has all states as outputs
879+ # TODO: check to make sure output map is the identity
880+ raise ControlArgument ("System output must be the full state" )
881+ else :
882+ # Use the system directly instead of an estimator
883+ estimator = sys
884+
885+ # See whether we should implement integral action
886+ nintegrators = 0
887+ if integral_action is not None :
888+ if not isinstance (integral_action , np .ndarray ):
889+ raise ControlArgument ("Integral action must pass an array" )
890+ elif integral_action .shape [1 ] != sys .nstates :
891+ raise ControlArgument (
892+ "Integral gain output size must match system input size" )
893+ else :
894+ nintegrators = integral_action .shape [0 ]
895+ C = integral_action
896+ else :
897+ # Create a C matrix with no outputs, just in case update gets called
898+ C = np .zeros ((0 , sys .nstates ))
899+
900+ # Check to make sure that state feedback has the right shape
901+ if not isinstance (K , np .ndarray ) or \
902+ K .shape != (sys .ninputs , estimator .noutputs + nintegrators ):
903+ raise ControlArgument (
904+ f'Control gain must be an array of size { sys .ninputs } '
905+ f'x { sys .nstates } ' +
906+ (f'+{ nintegrators } ' if nintegrators > 0 else '' ))
907+
908+ # Figure out the labels to use
909+ if isinstance (xd_labels , str ):
910+ # Gnerate the list of labels using the argument as a format string
911+ xd_labels = [xd_labels .format (i = i ) for i in range (sys .nstates )]
912+
913+ if isinstance (ud_labels , str ):
914+ # Gnerate the list of labels using the argument as a format string
915+ ud_labels = [ud_labels .format (i = i ) for i in range (sys .ninputs )]
916+
917+ # Define the controller system
918+ if type == 'nonlinear' :
919+ # Create an I/O system for the state feedback gains
920+ def _control_update (t , x , inputs , params ):
921+ # Split input into desired state, nominal input, and current state
922+ xd_vec = inputs [0 :sys .nstates ]
923+ x_vec = inputs [- estimator .nstates :]
924+
925+ # Compute the integral error in the xy coordinates
926+ return C @ x_vec - C @ xd_vec
927+
928+ def _control_output (t , e , z , params ):
929+ K = params .get ('K' )
930+
931+ # Split input into desired state, nominal input, and current state
932+ xd_vec = z [0 :sys .nstates ]
933+ ud_vec = z [sys .nstates :sys .nstates + sys .ninputs ]
934+ x_vec = z [- sys .nstates :]
935+
936+ # Compute the control law
937+ u = ud_vec - K [:, 0 :sys .nstates ] @ (x_vec - xd_vec )
938+ if nintegrators > 0 :
939+ u -= K [:, sys .nstates :] @ e
940+
941+ return u
942+
943+ ctrl = NonlinearIOSystem (
944+ _control_update , _control_output , name = 'control' ,
945+ inputs = xd_labels + ud_labels + estimator .output_labels ,
946+ outputs = list (sys .input_index .keys ()), params = {'K' : K },
947+ states = nintegrators )
948+
949+ elif type == 'linear' or type is None :
950+ # Create the matrices implementing the controller
951+ A_lqr = np .zeros ((C .shape [0 ], C .shape [0 ]))
952+ B_lqr = np .hstack ([- C , np .zeros ((C .shape [0 ], sys .ninputs )), C ])
953+ C_lqr = - K [:, sys .nstates :]
954+ D_lqr = np .hstack ([
955+ K [:, 0 :sys .nstates ], np .eye (sys .ninputs ), - K [:, 0 :sys .nstates ]
956+ ])
957+
958+ ctrl = ss (
959+ A_lqr , B_lqr , C_lqr , D_lqr , dt = sys .dt , name = 'control' ,
960+ inputs = xd_labels + ud_labels + estimator .output_labels ,
961+ outputs = list (sys .input_index .keys ()), states = nintegrators )
962+
963+ else :
964+ raise ControlArgument (f"unknown type '{ type } '" )
965+
966+ # Define the closed loop system
967+ closed = interconnect (
968+ [sys , ctrl ] if estimator == sys else [sys , ctrl , estimator ],
969+ name = sys .name + "_" + ctrl .name ,
970+ inplist = xd_labels + ud_labels , inputs = xd_labels + ud_labels ,
971+ outlist = sys .output_labels + sys .input_labels ,
972+ outputs = sys .output_labels + sys .input_labels
973+ )
974+ return ctrl , closed
975+
976+
788977def ctrb (A , B ):
789978 """Controllabilty matrix
790979
0 commit comments