4141
4242# External packages and modules
4343import numpy as np
44+ import scipy as sp
4445
4546from . import statesp
4647from .mateqn import care , dare , _check_shape
@@ -71,7 +72,7 @@ def sb03md(n, C, A, U, dico, job='X',fact='N',trana='N',ldwork=None):
7172 sb03od = None
7273
7374
74- __all__ = ['ctrb' , 'obsv' , 'gram' , 'place' , 'place_varga' , 'lqr' ,
75+ __all__ = ['ctrb' , 'obsv' , 'gram' , 'place' , 'place_varga' , 'lqr' ,
7576 'dlqr' , 'acker' , 'create_statefbk_iosystem' ]
7677
7778
@@ -596,8 +597,10 @@ def dlqr(*args, **kwargs):
596597
597598# Function to create an I/O sytems representing a state feedback controller
598599def create_statefbk_iosystem (
599- sys , K , integral_action = None , xd_labels = 'xd[{i}]' , ud_labels = 'ud[{i}]' ,
600- estimator = None , type = 'linear' ):
600+ sys , gain , integral_action = None , estimator = None , type = None ,
601+ xd_labels = 'xd[{i}]' , ud_labels = 'ud[{i}]' , gainsched_indices = None ,
602+ gainsched_method = 'linear' , name = None , inputs = None , outputs = None ,
603+ states = None ):
601604 """Create an I/O system using a (full) state feedback controller
602605
603606 This function creates an input/output system that implements a
@@ -613,31 +616,47 @@ def create_statefbk_iosystem(
613616 feedback gain (eg, from LQR). The function returns the controller
614617 ``ctrl`` and the closed loop systems ``clsys``, both as I/O systems.
615618
619+ A gain scheduled controller can also be created, by passing a list of
620+ gains and a corresponding list of values of a set of scheduling
621+ variables. In this case, the controller has the form
622+
623+ u = ud - K_p(mu) (x - xd) - K_i(mu) integral(C x - C x_d)
624+
625+ where mu represents the scheduling variable.
626+
616627 Parameters
617628 ----------
618629 sys : InputOutputSystem
619630 The I/O system that represents the process dynamics. If no estimator
620631 is given, the output of this system should represent the full state.
621632
622- K : ndarray
623- The state feedback gain. This matrix defines the gains to be
624- applied to the system. If ``integral_action`` is None, then the
625- dimensions of this array should be (sys.ninputs, sys.nstates). If
626- `integral action` is set to a matrix or a function, then additional
627- columns represent the gains of the integral states of the
628- controller.
633+ gain : ndarray or tuple
634+ If a array is give, it represents the state feedback gain (K).
635+ This matrix defines the gains to be applied to the system. If
636+ ``integral_action`` is None, then the dimensions of this array
637+ should be (sys.ninputs, sys.nstates). If `integral action` is
638+ set to a matrix or a function, then additional columns
639+ represent the gains of the integral states of the controller.
640+
641+ If a tuple is given, then it specifies a gain schedule. The tuple
642+ should be of the form ``(gains, points)`` where gains is a list of
643+ gains :math:`K_j` and points is a list of values :math:`\\ mu_j` at
644+ which the gains are computed. The `gainsched_indices` parameter
645+ should be used to specify the scheduling variables.
629646
630647 xd_labels, ud_labels : str or list of str, optional
631648 Set the name of the signals to use for the desired state and inputs.
632649 If a single string is specified, it should be a format string using
633- the variable ``i`` as an index. Otherwise, a list of strings matching
634- the size of xd and ud, respectively, should be used. Default is
635- ``'xd[{i}]'`` for xd_labels and ``'xd[{i}]'`` for ud_labels.
650+ the variable ``i`` as an index. Otherwise, a list of strings
651+ matching the size of xd and ud, respectively, should be used.
652+ Default is ``'xd[{i}]'`` for xd_labels and ``'ud[{i}]'`` for
653+ ud_labels. These settings can also be overriden using the `inputs`
654+ keyword.
636655
637656 integral_action : None, ndarray, or func, optional
638657 If this keyword is specified, the controller can include integral
639- action in addition to state feedback. If ``integral_action`` is an
640- ndarray , it will be multiplied by the current and desired state to
658+ action in addition to state feedback. If ``integral_action`` is a
659+ matrix , it will be multiplied by the current and desired state to
641660 generate the error for the internal integrator states of the control
642661 law. If ``integral_action`` is a function ``h``, that function will
643662 be called with the signature h(t, x, u, params) to obtain the
@@ -646,30 +665,63 @@ def create_statefbk_iosystem(
646665 ``K`` matrix.
647666
648667 estimator : InputOutputSystem, optional
649- If an estimator is provided, using the states of the estimator as
668+ If an estimator is provided, use the states of the estimator as
650669 the system inputs for the controller.
651670
652- type : 'nonlinear' or 'linear', optional
653- Set the type of controller to create. The default is a linear
654- controller implementing the LQR regulator. If the type is 'nonlinear',
655- a :class:NonlinearIOSystem is created instead, with the gain ``K`` as
656- a parameter (allowing modifications of the gain at runtime).
671+ gainsched_indices : list of int or str, optional
672+ If a gain scheduled controller is specified, specify the indices of
673+ the controller input to use for scheduling the gain. The input to
674+ the controller is the desired state xd, the desired input ud, and
675+ the system state x (or state estimate xhat, if an estimator is
676+ given). The indices can either be specified as integer offsets into
677+ the input vector or as strings matching the signal names of the
678+ input vector. The default is to use the desire state xd.
679+
680+ gainsched_method : str, optional
681+ The method to use for gain scheduling. Possible values are 'linear'
682+ (default), 'nearest', and 'cubic'. More information is available in
683+ :func:`scipy.interpolate.griddata`. For points outside of the convex
684+ hull of the scheduling points, the gain at the nearest point is
685+ used.
686+
687+ type : 'linear' or 'nonlinear', optional
688+ Set the type of controller to create. The default for a linear gain
689+ is a linear controller implementing the LQR regulator. If the type
690+ is 'nonlinear', a :class:NonlinearIOSystem is created instead, with
691+ the gain ``K`` as a parameter (allowing modifications of the gain at
692+ runtime). If the gain parameter is a tuple, then a nonlinear,
693+ gain-scheduled controller is created.
657694
658695 Returns
659696 -------
660697 ctrl : InputOutputSystem
661698 Input/output system representing the controller. This system takes
662- as inputs the desired state xd, the desired input ud, and the system
663- state x. It outputs the controller action u according to the
664- formula u = ud - K(x - xd). If the keyword `integral_action` is
665- specified, then an additional set of integrators is included in the
666- control system (with the gain matrix K having the integral gains
667- appended after the state gains).
699+ as inputs the desired state ``xd``, the desired input ``ud``, and
700+ either the system state ``x`` or the estimated state ``xhat``. It
701+ outputs the controller action u according to the formula :math:`u =
702+ u_d - K(x - x_d)`. If the keyword ``integral_action`` is specified,
703+ then an additional set of integrators is included in the control
704+ system (with the gain matrix ``K`` having the integral gains
705+ appended after the state gains). If a gain scheduled controller is
706+ specified, the gain (proportional and integral) are evaluated using
707+ the scheduling variables specified by ``gainsched_indices``.
668708
669709 clsys : InputOutputSystem
670710 Input/output system representing the closed loop system. This
671- systems takes as inputs the desired trajectory (xd, ud) and outputs
672- the system state x and the applied input u (vertically stacked).
711+ systems takes as inputs the desired trajectory ``(xd, ud)`` and
712+ outputs the system state ``x`` and the applied input ``u``
713+ (vertically stacked).
714+
715+ Other Parameters
716+ ----------------
717+ inputs, outputs : str, or list of str, optional
718+ List of strings that name the individual signals of the transformed
719+ system. If not given, the inputs and outputs are the same as the
720+ original system.
721+
722+ name : string, optional
723+ System name. If unspecified, a generic name <sys[id]> is generated
724+ with a unique integer id.
673725
674726 """
675727 # Make sure that we were passed an I/O system as an input
@@ -705,53 +757,127 @@ def create_statefbk_iosystem(
705757 C = np .zeros ((0 , sys .nstates ))
706758
707759 # Check to make sure that state feedback has the right shape
708- if not isinstance (K , np .ndarray ) or \
709- K .shape != (sys .ninputs , estimator .noutputs + nintegrators ):
760+ if isinstance (gain , np .ndarray ):
761+ K = gain
762+ if K .shape != (sys .ninputs , estimator .noutputs + nintegrators ):
763+ raise ControlArgument (
764+ f'Control gain must be an array of size { sys .ninputs } '
765+ f'x { sys .nstates } ' +
766+ (f'+{ nintegrators } ' if nintegrators > 0 else '' ))
767+ gainsched = False
768+
769+ elif isinstance (gain , tuple ):
770+ # Check for gain scheduled controller
771+ if len (gain ) != 2 :
772+ raise ControlArgument ("gain must be a 2-tuple for gain scheduling" )
773+ gains , points = gain [0 :2 ]
774+
775+ # Stack gains and points if past as a list
776+ gains = np .stack (gains )
777+ points = np .stack (points )
778+ gainsched = True
779+
780+ else :
781+ raise ControlArgument ("gain must be an array or a tuple" )
782+
783+ # Decide on the type of system to create
784+ if gainsched and type == 'linear' :
710785 raise ControlArgument (
711- f'Control gain must be an array of size { sys .ninputs } '
712- f'x { sys .nstates } ' +
713- (f'+{ nintegrators } ' if nintegrators > 0 else '' ))
786+ "type 'linear' not allowed for gain scheduled controller" )
787+ elif type is None :
788+ type = 'nonlinear' if gainsched else 'linear'
789+ elif type not in {'linear' , 'nonlinear' }:
790+ raise ControlArgument (f"unknown type '{ type } '" )
714791
715792 # Figure out the labels to use
716793 if isinstance (xd_labels , str ):
717- # Gnerate the list of labels using the argument as a format string
794+ # Generate the list of labels using the argument as a format string
718795 xd_labels = [xd_labels .format (i = i ) for i in range (sys .nstates )]
719796
720797 if isinstance (ud_labels , str ):
721- # Gnerate the list of labels using the argument as a format string
798+ # Generate the list of labels using the argument as a format string
722799 ud_labels = [ud_labels .format (i = i ) for i in range (sys .ninputs )]
723800
801+ # Create the signal and system names
802+ if inputs is None :
803+ inputs = xd_labels + ud_labels + estimator .output_labels
804+ if outputs is None :
805+ outputs = list (sys .input_index .keys ())
806+ if states is None :
807+ states = nintegrators
808+
809+ # Process gainscheduling variables, if present
810+ if gainsched :
811+ # Create a copy of the scheduling variable indices (default = xd)
812+ gainsched_indices = range (sys .nstates ) if gainsched_indices is None \
813+ else list (gainsched_indices )
814+
815+ # Make sure the scheduling variable indices are the right length
816+ if len (gainsched_indices ) != points .shape [1 ]:
817+ raise ControlArgument (
818+ "length of gainsched_indices must match dimension of"
819+ " scheduling variables" )
820+
821+ # Process scheduling variables
822+ for i , idx in enumerate (gainsched_indices ):
823+ if isinstance (idx , str ):
824+ gainsched_indices [i ] = inputs .index (gainsched_indices [i ])
825+
826+ # Create interpolating function
827+ if gainsched_method == 'nearest' :
828+ _interp = sp .interpolate .NearestNDInterpolator (points , gains )
829+ def _nearest (mu ):
830+ raise SystemError (f"could not find nearest gain at mu = { mu } " )
831+ elif gainsched_method == 'linear' :
832+ _interp = sp .interpolate .LinearNDInterpolator (points , gains )
833+ _nearest = sp .interpolate .NearestNDInterpolator (points , gains )
834+ elif gainsched_method == 'cubic' :
835+ _interp = sp .interpolate .CloughTocher2DInterpolator (points , gains )
836+ _nearest = sp .interpolate .NearestNDInterpolator (points , gains )
837+ else :
838+ raise ControlArgument (
839+ f"unknown gain scheduling method '{ gainsched_method } '" )
840+
841+ def _compute_gain (mu ):
842+ K = _interp (mu )
843+ if np .isnan (K ).any ():
844+ K = _nearest (mu )
845+ return K
846+
724847 # Define the controller system
725848 if type == 'nonlinear' :
726849 # Create an I/O system for the state feedback gains
727- def _control_update (t , x , inputs , params ):
850+ def _control_update (t , states , inputs , params ):
728851 # Split input into desired state, nominal input, and current state
729852 xd_vec = inputs [0 :sys .nstates ]
730853 x_vec = inputs [- estimator .nstates :]
731854
732855 # Compute the integral error in the xy coordinates
733- return C @ x_vec - C @ xd_vec
856+ return C @ ( x_vec - xd_vec )
734857
735- def _control_output (t , e , z , params ):
736- K = params .get ('K' )
858+ def _control_output (t , states , inputs , params ):
859+ if gainsched :
860+ mu = inputs [gainsched_indices ]
861+ K_ = _compute_gain (mu )
862+ else :
863+ K_ = params .get ('K' )
737864
738865 # Split input into desired state, nominal input, and current state
739- xd_vec = z [0 :sys .nstates ]
740- ud_vec = z [sys .nstates :sys .nstates + sys .ninputs ]
741- x_vec = z [- sys .nstates :]
866+ xd_vec = inputs [0 :sys .nstates ]
867+ ud_vec = inputs [sys .nstates :sys .nstates + sys .ninputs ]
868+ x_vec = inputs [- sys .nstates :]
742869
743870 # Compute the control law
744- u = ud_vec - K [:, 0 :sys .nstates ] @ (x_vec - xd_vec )
871+ u = ud_vec - K_ [:, 0 :sys .nstates ] @ (x_vec - xd_vec )
745872 if nintegrators > 0 :
746- u -= K [:, sys .nstates :] @ e
873+ u -= K_ [:, sys .nstates :] @ states
747874
748875 return u
749876
877+ params = {} if gainsched else {'K' : K }
750878 ctrl = NonlinearIOSystem (
751- _control_update , _control_output , name = 'control' ,
752- inputs = xd_labels + ud_labels + estimator .output_labels ,
753- outputs = list (sys .input_index .keys ()), params = {'K' : K },
754- states = nintegrators )
879+ _control_update , _control_output , name = name , inputs = inputs ,
880+ outputs = outputs , states = states , params = params )
755881
756882 elif type == 'linear' or type is None :
757883 # Create the matrices implementing the controller
@@ -768,9 +894,8 @@ def _control_output(t, e, z, params):
768894 ])
769895
770896 ctrl = ss (
771- A_lqr , B_lqr , C_lqr , D_lqr , dt = sys .dt , name = 'control' ,
772- inputs = xd_labels + ud_labels + estimator .output_labels ,
773- outputs = list (sys .input_index .keys ()), states = nintegrators )
897+ A_lqr , B_lqr , C_lqr , D_lqr , dt = sys .dt , name = name ,
898+ inputs = inputs , outputs = outputs , states = states )
774899
775900 else :
776901 raise ControlArgument (f"unknown type '{ type } '" )
@@ -779,7 +904,7 @@ def _control_output(t, e, z, params):
779904 closed = interconnect (
780905 [sys , ctrl ] if estimator == sys else [sys , ctrl , estimator ],
781906 name = sys .name + "_" + ctrl .name ,
782- inplist = xd_labels + ud_labels , inputs = xd_labels + ud_labels ,
907+ inplist = inputs [: - sys . nstates ] , inputs = inputs [: - sys . nstates ] ,
783908 outlist = sys .output_labels + sys .input_labels ,
784909 outputs = sys .output_labels + sys .input_labels
785910 )
0 commit comments