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
@@ -600,8 +601,8 @@ def dlqr(*args, **kwargs):
600601
601602# Function to create an I/O sytems representing a state feedback controller
602603def create_statefbk_iosystem (
603- sys , K , integral_action = None , xd_labels = 'xd[{i}]' , ud_labels = 'ud[{i}]' ,
604- estimator = None , type = 'linear' ):
604+ sys , gain , integral_action = None , estimator = None , type = None ,
605+ xd_labels = 'xd[{i}]' , ud_labels = 'ud[{i}]' , gainsched_indices = None ):
605606 """Create an I/O system using a (full) state feedback controller
606607
607608 This function creates an input/output system that implements a
@@ -617,26 +618,46 @@ def create_statefbk_iosystem(
617618 feedback gain (eg, from LQR). The function returns the controller
618619 ``ctrl`` and the closed loop systems ``clsys``, both as I/O systems.
619620
621+ A gain scheduled controller can also be created, by passing a list of
622+ gains and a corresponding list of values of a set of scheduling
623+ variables. In this case, the controller has the form
624+
625+ u = ud - K_p(mu) (x - xd) - K_i(mu) integral(C x - C x_d)
626+
627+ where mu represents the scheduling variable.
628+
620629 Parameters
621630 ----------
622631 sys : InputOutputSystem
623632 The I/O system that represents the process dynamics. If no estimator
624633 is given, the output of this system should represent the full state.
625634
626- K : ndarray
627- The state feedback gain. This matrix defines the gains to be
628- applied to the system. If ``integral_action`` is None, then the
629- dimensions of this array should be (sys.ninputs, sys.nstates). If
630- `integral action` is set to a matrix or a function, then additional
631- columns represent the gains of the integral states of the
632- controller.
635+ gain : ndarray or tuple
636+ If a array is give, it represents the state feedback gain (K).
637+ This matrix defines the gains to be applied to the system. If
638+ ``integral_action`` is None, then the dimensions of this array
639+ should be (sys.ninputs, sys.nstates). If `integral action` is
640+ set to a matrix or a function, then additional columns
641+ represent the gains of the integral states of the controller.
642+
643+ If a tuple is given, then it specifies a gain schedule. The
644+ tuple should be of the form
645+
646+ (gains, points[, method])
647+
648+ where gains is a list of gains :math:`K_j` and points is a list of
649+ values :math:`\\ mu_j` at which the gains are computed. If `method`
650+ is specified, it is passed to :func:`scipy.interpolate.griddata` to
651+ specify the method of interpolation. Possible values include
652+ `linear`, `nearest`, and `cubic`.
633653
634654 xd_labels, ud_labels : str or list of str, optional
635655 Set the name of the signals to use for the desired state and inputs.
636656 If a single string is specified, it should be a format string using
637- the variable ``i`` as an index. Otherwise, a list of strings matching
638- the size of xd and ud, respectively, should be used. Default is
639- ``'xd[{i}]'`` for xd_labels and ``'xd[{i}]'`` for ud_labels.
657+ the variable ``i`` as an index. Otherwise, a list of strings
658+ matching the size of xd and ud, respectively, should be used.
659+ Default is ``'xd[{i}]'`` for xd_labels and ``'ud[{i}]'`` for
660+ ud_labels.
640661
641662 integral_action : None, ndarray, or func, optional
642663 If this keyword is specified, the controller can include integral
@@ -650,30 +671,48 @@ def create_statefbk_iosystem(
650671 ``K`` matrix.
651672
652673 estimator : InputOutputSystem, optional
653- If an estimator is provided, using the states of the estimator as
674+ If an estimator is provided, use the states of the estimator as
654675 the system inputs for the controller.
655676
656- type : 'nonlinear' or 'linear', optional
657- Set the type of controller to create. The default is a linear
658- controller implementing the LQR regulator. If the type is 'nonlinear',
659- a :class:NonlinearIOSystem is created instead, with the gain ``K`` as
660- a parameter (allowing modifications of the gain at runtime).
677+ gainsched_indices : list of integers, optional
678+ If a gain scheduled controller is specified, specify the indices of
679+ the controller input to use for scheduling the gain. The input to
680+ the controller is the desired state xd, the desired input ud, and
681+ either the system state x or the system output y (if an estimator is
682+ given).
683+
684+ type : 'linear' or 'nonlinear', optional
685+ Set the type of controller to create. The default for a linear gain
686+ is a linear controller implementing the LQR regulator. If the type
687+ is 'nonlinear', a :class:NonlinearIOSystem is created instead, with
688+ the gain ``K`` as a parameter (allowing modifications of the gain at
689+ runtime). If the gain parameter is a tuple, the a nonlinear,
690+ gain-scheduled controller is created.
661691
662692 Returns
663693 -------
664694 ctrl : InputOutputSystem
665695 Input/output system representing the controller. This system takes
666- as inputs the desired state xd, the desired input ud, and the system
667- state x. It outputs the controller action u according to the
668- formula u = ud - K(x - xd). If the keyword `integral_action` is
669- specified, then an additional set of integrators is included in the
670- control system (with the gain matrix K having the integral gains
671- appended after the state gains).
696+ as inputs the desired state xd, the desired input ud, and either the
697+ system state x or the system output y (if an estimator is given).
698+ It outputs the controller action u according to the formula u = ud -
699+ K(x - xd). If the keyword `integral_action` is specified, then an
700+ additional set of integrators is included in the control system
701+ (with the gain matrix K having the integral gains appended after the
702+ state gains). If a gain scheduled controller is specified, the gain
703+ (proportional and integral) are evaluated using the input mu.
672704
673705 clsys : InputOutputSystem
674706 Input/output system representing the closed loop system. This
675- systems takes as inputs the desired trajectory (xd, ud) and outputs
676- the system state x and the applied input u (vertically stacked).
707+ systems takes as inputs the desired trajectory (xd, ud) along with
708+ any unassigned gain scheduling values mu and outputs the system
709+ state x and the applied input u (vertically stacked).
710+
711+ Notes
712+ -----
713+ 1. If the gain scheduling variable labes are set to the names of system
714+ states, inputs, or outputs or desired states or inputs, then the
715+ scheduling variables are internally connected to those variables.
677716
678717 """
679718 # Make sure that we were passed an I/O system as an input
@@ -709,52 +748,104 @@ def create_statefbk_iosystem(
709748 C = np .zeros ((0 , sys .nstates ))
710749
711750 # Check to make sure that state feedback has the right shape
712- if not isinstance (K , np .ndarray ) or \
713- K .shape != (sys .ninputs , estimator .noutputs + nintegrators ):
751+ if isinstance (gain , np .ndarray ):
752+ K = gain
753+ if K .shape != (sys .ninputs , estimator .noutputs + nintegrators ):
754+ raise ControlArgument (
755+ f'Control gain must be an array of size { sys .ninputs } '
756+ f'x { sys .nstates } ' +
757+ (f'+{ nintegrators } ' if nintegrators > 0 else '' ))
758+ gainsched = False
759+
760+ elif isinstance (gain , tuple ):
761+ # Check for gain scheduled controller
762+ gains , points = gain [0 :2 ]
763+ method = 'nearest' if len (gain ) < 3 else gain [2 ]
764+
765+ # Stack gains and points if past as a list
766+ gains = np .stack (gains )
767+ points = np .stack (points )
768+ gainsched = True
769+
770+ else :
771+ raise ControlArgument ("gain must be an array or a tuple" )
772+
773+ # Decide on the type of system to create
774+ if gainsched and type == 'linear' :
714775 raise ControlArgument (
715- f'Control gain must be an array of size { sys .ninputs } '
716- f'x { sys .nstates } ' +
717- (f'+{ nintegrators } ' if nintegrators > 0 else '' ))
776+ "type 'linear' not allowed for gain scheduled controller" )
777+ elif type is None :
778+ type = 'nonlinear' if gainsched else 'linear'
779+ elif type not in {'linear' , 'nonlinear' }:
780+ raise ControlArgument (f"unknown type '{ type } '" )
718781
719782 # Figure out the labels to use
720783 if isinstance (xd_labels , str ):
721- # Gnerate the list of labels using the argument as a format string
784+ # Generate the list of labels using the argument as a format string
722785 xd_labels = [xd_labels .format (i = i ) for i in range (sys .nstates )]
723786
724787 if isinstance (ud_labels , str ):
725- # Gnerate the list of labels using the argument as a format string
788+ # Generate the list of labels using the argument as a format string
726789 ud_labels = [ud_labels .format (i = i ) for i in range (sys .ninputs )]
727790
791+ # Process gainscheduling variables, if present
792+ if gainsched :
793+ # Create a copy of the scheduling variable indices (default = empty)
794+ gainsched_indices = [] if gainsched_indices is None \
795+ else list (gainsched_indices )
796+
797+ # Make sure the scheduling variable indices are the right length
798+ if len (gainsched_indices ) != points .shape [1 ]:
799+ raise ControlArgument (
800+ "Length of gainsched_indices must match dimension of"
801+ " scheduling variables" )
802+
803+ # TODO: Process scheduling variables
804+
728805 # Define the controller system
729806 if type == 'nonlinear' :
730807 # Create an I/O system for the state feedback gains
731- def _control_update (t , x , inputs , params ):
808+ def _control_update (t , states , inputs , params ):
732809 # Split input into desired state, nominal input, and current state
733810 xd_vec = inputs [0 :sys .nstates ]
734811 x_vec = inputs [- estimator .nstates :]
735812
736813 # Compute the integral error in the xy coordinates
737- return C @ x_vec - C @ xd_vec
814+ return C @ (x_vec - xd_vec )
815+
816+ def _compute_gain (mu , gains_ , points_ ):
817+ K = np .array ([
818+ [sp .interpolate .griddata (
819+ points_ , gains_ [:, i , j ], mu , method = method ).item ()
820+ for j in range (gains_ .shape [2 ])]
821+ for i in range (gains_ .shape [1 ])
822+ ])
823+ return K
738824
739- def _control_output (t , e , z , params ):
740- K = params .get ('K' )
825+ def _control_output (t , states , inputs , params ):
826+ if gainsched :
827+ mu = inputs [gainsched_indices ]
828+ K_ = _compute_gain (mu , gains , points )
829+ else :
830+ K_ = params .get ('K' )
741831
742832 # Split input into desired state, nominal input, and current state
743- xd_vec = z [0 :sys .nstates ]
744- ud_vec = z [sys .nstates :sys .nstates + sys .ninputs ]
745- x_vec = z [- sys .nstates :]
833+ xd_vec = inputs [0 :sys .nstates ]
834+ ud_vec = inputs [sys .nstates :sys .nstates + sys .ninputs ]
835+ x_vec = inputs [- sys .nstates :]
746836
747837 # Compute the control law
748- u = ud_vec - K [:, 0 :sys .nstates ] @ (x_vec - xd_vec )
838+ u = ud_vec - K_ [:, 0 :sys .nstates ] @ (x_vec - xd_vec )
749839 if nintegrators > 0 :
750- u -= K [:, sys .nstates :] @ e
840+ u -= K_ [:, sys .nstates :] @ states
751841
752842 return u
753843
844+ params = {} if gainsched else {'K' : K }
754845 ctrl = NonlinearIOSystem (
755846 _control_update , _control_output , name = 'control' ,
756847 inputs = xd_labels + ud_labels + estimator .output_labels ,
757- outputs = list (sys .input_index .keys ()), params = { 'K' : K } ,
848+ outputs = list (sys .input_index .keys ()), params = params ,
758849 states = nintegrators )
759850
760851 elif type == 'linear' or type is None :
0 commit comments