41
41
42
42
# External packages and modules
43
43
import numpy as np
44
+ import scipy as sp
44
45
45
46
from . import statesp
46
47
from .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):
71
72
sb03od = None
72
73
73
74
74
- __all__ = ['ctrb' , 'obsv' , 'gram' , 'place' , 'place_varga' , 'lqr' ,
75
+ __all__ = ['ctrb' , 'obsv' , 'gram' , 'place' , 'place_varga' , 'lqr' ,
75
76
'dlqr' , 'acker' , 'create_statefbk_iosystem' ]
76
77
77
78
@@ -596,8 +597,10 @@ def dlqr(*args, **kwargs):
596
597
597
598
# Function to create an I/O sytems representing a state feedback controller
598
599
def 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 ):
601
604
"""Create an I/O system using a (full) state feedback controller
602
605
603
606
This function creates an input/output system that implements a
@@ -613,31 +616,47 @@ def create_statefbk_iosystem(
613
616
feedback gain (eg, from LQR). The function returns the controller
614
617
``ctrl`` and the closed loop systems ``clsys``, both as I/O systems.
615
618
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
+
616
627
Parameters
617
628
----------
618
629
sys : InputOutputSystem
619
630
The I/O system that represents the process dynamics. If no estimator
620
631
is given, the output of this system should represent the full state.
621
632
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.
629
646
630
647
xd_labels, ud_labels : str or list of str, optional
631
648
Set the name of the signals to use for the desired state and inputs.
632
649
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.
636
655
637
656
integral_action : None, ndarray, or func, optional
638
657
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
641
660
generate the error for the internal integrator states of the control
642
661
law. If ``integral_action`` is a function ``h``, that function will
643
662
be called with the signature h(t, x, u, params) to obtain the
@@ -646,30 +665,63 @@ def create_statefbk_iosystem(
646
665
``K`` matrix.
647
666
648
667
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
650
669
the system inputs for the controller.
651
670
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.
657
694
658
695
Returns
659
696
-------
660
697
ctrl : InputOutputSystem
661
698
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``.
668
708
669
709
clsys : InputOutputSystem
670
710
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.
673
725
674
726
"""
675
727
# Make sure that we were passed an I/O system as an input
@@ -705,53 +757,127 @@ def create_statefbk_iosystem(
705
757
C = np .zeros ((0 , sys .nstates ))
706
758
707
759
# 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' :
710
785
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 } '" )
714
791
715
792
# Figure out the labels to use
716
793
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
718
795
xd_labels = [xd_labels .format (i = i ) for i in range (sys .nstates )]
719
796
720
797
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
722
799
ud_labels = [ud_labels .format (i = i ) for i in range (sys .ninputs )]
723
800
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
+
724
847
# Define the controller system
725
848
if type == 'nonlinear' :
726
849
# 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 ):
728
851
# Split input into desired state, nominal input, and current state
729
852
xd_vec = inputs [0 :sys .nstates ]
730
853
x_vec = inputs [- estimator .nstates :]
731
854
732
855
# Compute the integral error in the xy coordinates
733
- return C @ x_vec - C @ xd_vec
856
+ return C @ ( x_vec - xd_vec )
734
857
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' )
737
864
738
865
# 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 :]
742
869
743
870
# 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 )
745
872
if nintegrators > 0 :
746
- u -= K [:, sys .nstates :] @ e
873
+ u -= K_ [:, sys .nstates :] @ states
747
874
748
875
return u
749
876
877
+ params = {} if gainsched else {'K' : K }
750
878
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 )
755
881
756
882
elif type == 'linear' or type is None :
757
883
# Create the matrices implementing the controller
@@ -768,9 +894,8 @@ def _control_output(t, e, z, params):
768
894
])
769
895
770
896
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 )
774
899
775
900
else :
776
901
raise ControlArgument (f"unknown type '{ type } '" )
@@ -779,7 +904,7 @@ def _control_output(t, e, z, params):
779
904
closed = interconnect (
780
905
[sys , ctrl ] if estimator == sys else [sys , ctrl , estimator ],
781
906
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 ] ,
783
908
outlist = sys .output_labels + sys .input_labels ,
784
909
outputs = sys .output_labels + sys .input_labels
785
910
)
0 commit comments