@@ -411,7 +411,7 @@ def point_to_point(
411411 # Solve for the coefficients of the flat outputs
412412 #
413413 # At this point, we need to solve the equation M alpha = zflag, where M
414- # is the matrix constrains for initial and final conditions and zflag =
414+ # is the matrix constraints for initial and final conditions and zflag =
415415 # [zflag_T0; zflag_tf].
416416 #
417417 # If there are no constraints, then we just need to solve a linear
@@ -426,6 +426,11 @@ def point_to_point(
426426 if rank < Z .size :
427427 warnings .warn ("basis too small; solution may not exist" )
428428
429+ # Precompute the collocation matrix the defines the flag at timepts
430+ Mt_list = []
431+ for t in timepts :
432+ Mt_list .append (_basis_flag_matrix (sys , basis , zflag_T0 , t ))
433+
429434 if cost is not None or trajectory_constraints is not None :
430435 # Search over the null space to minimize cost/satisfy constraints
431436 N = sp .linalg .null_space (M )
@@ -438,8 +443,8 @@ def traj_cost(null_coeffs):
438443 # Evaluate the costs at the listed time points
439444 # TODO: store Mt ahead of time, since it doesn't change
440445 costval = 0
441- for t in timepts :
442- M_t = _basis_flag_matrix ( sys , basis , zflag_T0 , t )
446+ for i , t in enumerate ( timepts ) :
447+ M_t = Mt_list [ i ]
443448
444449 # Compute flag at this time point
445450 zflag = (M_t @ coeffs ).reshape (sys .ninputs , - 1 )
@@ -477,7 +482,7 @@ def traj_const(null_coeffs):
477482 values = []
478483 for i , t in enumerate (timepts ):
479484 # Calculate the states and inputs for the flat output
480- M_t = _basis_flag_matrix ( sys , basis , zflag_T0 , t )
485+ M_t = Mt_list [ i ]
481486
482487 # Compute flag at this time point
483488 zflag = (M_t @ coeffs ).reshape (sys .ninputs , - 1 )
@@ -551,3 +556,306 @@ def traj_const(null_coeffs):
551556
552557 # Return a function that computes inputs and states as a function of time
553558 return systraj
559+
560+
561+ # Solve a point to point trajectory generation problem for a flat system
562+ def solve_flat_ocp (
563+ sys , timepts , x0 = 0 , u0 = 0 , basis = None , trajectory_cost = None ,
564+ terminal_cost = None , trajectory_constraints = None ,
565+ initial_guess = None , params = None , ** kwargs ):
566+ """Compute trajectory between an initial and final conditions.
567+
568+ Compute an optimial trajectory for a differentially flat system starting
569+ from an initial state and input value.
570+
571+ Parameters
572+ ----------
573+ flatsys : FlatSystem object
574+ Description of the differentially flat system. This object must
575+ define a function `flatsys.forward()` that takes the system state and
576+ produceds the flag of flat outputs and a system `flatsys.reverse()`
577+ that takes the flag of the flat output and prodes the state and
578+ input.
579+
580+ timepts : float or 1D array_like
581+ The list of points for evaluating cost and constraints, as well as
582+ the time horizon. If given as a float, indicates the final time for
583+ the trajectory (corresponding to xf)
584+
585+ x0, u0 : 1D arrays
586+ Define the initial conditions for the system. If either of the
587+ values are given as None, they are replaced by a vector of zeros of
588+ the appropriate dimension.
589+
590+ basis : :class:`~control.flatsys.BasisFamily` object, optional
591+ The basis functions to use for generating the trajectory. If not
592+ specified, the :class:`~control.flatsys.PolyFamily` basis family
593+ will be used, with the minimal number of elements required to find a
594+ feasible trajectory (twice the number of system states)
595+
596+ trajectory_cost : callable
597+ Function that returns the integral cost given the current state
598+ and input. Called as `cost(x, u)`.
599+
600+ terminal_cost : callable
601+ Function that returns the terminal cost given the state and input.
602+ Called as `cost(x, u)`.
603+
604+ trajectory_constraints : list of tuples, optional
605+ List of constraints that should hold at each point in the time vector.
606+ Each element of the list should consist of a tuple with first element
607+ given by :class:`scipy.optimize.LinearConstraint` or
608+ :class:`scipy.optimize.NonlinearConstraint` and the remaining
609+ elements of the tuple are the arguments that would be passed to those
610+ functions. The following tuples are supported:
611+
612+ * (LinearConstraint, A, lb, ub): The matrix A is multiplied by stacked
613+ vector of the state and input at each point on the trajectory for
614+ comparison against the upper and lower bounds.
615+
616+ * (NonlinearConstraint, fun, lb, ub): a user-specific constraint
617+ function `fun(x, u)` is called at each point along the trajectory
618+ and compared against the upper and lower bounds.
619+
620+ The constraints are applied at each time point along the trajectory.
621+
622+ minimize_kwargs : str, optional
623+ Pass additional keywords to :func:`scipy.optimize.minimize`.
624+
625+ Returns
626+ -------
627+ traj : :class:`~control.flatsys.SystemTrajectory` object
628+ The system trajectory is returned as an object that implements the
629+ `eval()` function, we can be used to compute the value of the state
630+ and input and a given time t.
631+
632+ Notes
633+ -----
634+ Additional keyword parameters can be used to fine tune the behavior of
635+ the underlying optimization function. See `minimize_*` keywords in
636+ :func:`OptimalControlProblem` for more information.
637+
638+ """
639+ #
640+ # Make sure the problem is one that we can handle
641+ #
642+ x0 = _check_convert_array (x0 , [(sys .nstates ,), (sys .nstates , 1 )],
643+ 'Initial state: ' , squeeze = True )
644+ u0 = _check_convert_array (u0 , [(sys .ninputs ,), (sys .ninputs , 1 )],
645+ 'Initial input: ' , squeeze = True )
646+
647+ # Process final time
648+ timepts = np .atleast_1d (timepts )
649+ Tf = timepts [- 1 ]
650+ T0 = timepts [0 ] if len (timepts ) > 1 else T0
651+
652+ # Process keyword arguments
653+ if trajectory_constraints is None :
654+ # Backwards compatibility
655+ trajectory_constraints = kwargs .pop ('constraints' , None )
656+ if trajectory_cost is None :
657+ # Compatibility with point_to_point
658+ trajectory_cost = kwargs .pop ('cost' , None )
659+
660+ minimize_kwargs = {}
661+ minimize_kwargs ['method' ] = kwargs .pop ('minimize_method' , None )
662+ minimize_kwargs ['options' ] = kwargs .pop ('minimize_options' , {})
663+ minimize_kwargs .update (kwargs .pop ('minimize_kwargs' , {}))
664+
665+ if trajectory_cost is None and terminal_cost is None :
666+ raise TypeError ("need trajectory and/or terminal cost required" )
667+
668+ if kwargs :
669+ raise TypeError ("unrecognized keywords: " , str (kwargs ))
670+
671+ #
672+ # Determine the basis function set to use and make sure it is big enough
673+ #
674+
675+ # If no basis set was specified, use a polynomial basis (poor choice...)
676+ if basis is None :
677+ basis = PolyFamily (2 * (sys .nstates + sys .ninputs ))
678+
679+ # If a multivariable basis was given, make sure the size is correct
680+ if basis .nvars is not None and basis .nvars != sys .ninputs :
681+ raise ValueError ("size of basis does not match flat system size" )
682+
683+ # Make sure we have enough basis functions to solve the problem
684+ ncoefs = sum ([basis .var_ncoefs (i ) for i in range (sys .ninputs )])
685+ if ncoefs <= sys .nstates + sys .ninputs :
686+ raise ValueError ("basis set is too small" )
687+
688+ # Figure out the parameters to use, if any
689+ params = sys .params if params is None else params
690+
691+ #
692+ # Map the initial and conditions to flat output conditions
693+ #
694+ # We need to compute the output "flag": [z(t), z'(t), z''(t), ...]
695+ # and then evaluate this at the initial and final condition.
696+ #
697+
698+ zflag_T0 = sys .forward (x0 , u0 , params )
699+ Z_T0 = np .hstack (zflag_T0 )
700+
701+ #
702+ # Compute the matrix constraints for initial conditions
703+ #
704+ # This computation depends on the basis function we are using. It
705+ # essentially amounts to evaluating the basis functions and their
706+ # derivatives at the initial conditions.
707+
708+ # Compute the flags for the initial and final states
709+ M_T0 = _basis_flag_matrix (sys , basis , zflag_T0 , T0 )
710+
711+ #
712+ # Solve for the coefficients of the flat outputs
713+ #
714+ # At this point, we need to solve the equation M_T0 alpha = zflag_T0.
715+ #
716+ # If there are no additional constraints, then we just need to solve a
717+ # linear system of equations => use least squares. Otherwise, we have a
718+ # nonlinear optimal control problem with equality constraints => use
719+ # scipy.optimize.minimize().
720+ #
721+
722+ # Start by solving the least squares problem
723+ alpha , residuals , rank , s = np .linalg .lstsq (M_T0 , Z_T0 , rcond = None )
724+ if rank < Z_T0 .size :
725+ warnings .warn ("basis too small; solution may not exist" )
726+
727+ # Precompute the collocation matrix the defines the flag at timepts
728+ # TODO: only compute if we have trajectory cost/constraints
729+ Mt_list = []
730+ for t in timepts :
731+ Mt_list .append (_basis_flag_matrix (sys , basis , zflag_T0 , t ))
732+
733+ # Search over the null space to minimize cost/satisfy constraints
734+ N = sp .linalg .null_space (M_T0 )
735+
736+ # Define a function to evaluate the cost along a trajectory
737+ def traj_cost (null_coeffs ):
738+ # Add this to the existing solution
739+ coeffs = alpha + N @ null_coeffs
740+ costval = 0
741+
742+ # Evaluate the trajectory costs at the listed time points
743+ if trajectory_cost is not None :
744+ for i , t in enumerate (timepts [0 :- 1 ]):
745+ M_t = Mt_list [i ]
746+
747+ # Compute flag at this time point
748+ zflag = (M_t @ coeffs ).reshape (sys .ninputs , - 1 )
749+
750+ # Find states and inputs at the time points
751+ x , u = sys .reverse (zflag , params )
752+
753+ # Evaluate the cost at this time point
754+ # TODO: make use of time interval
755+ costval += trajectory_cost (x , u )
756+
757+ # Evaluate the terminal_cost
758+ if terminal_cost is not None :
759+ M_t = Mt_list [- 1 ]
760+ zflag = (M_t @ coeffs ).reshape (sys .ninputs , - 1 )
761+ x , u = sys .reverse (zflag , params )
762+ costval += terminal_cost (x , u )
763+
764+ return costval
765+
766+ # Process the constraints we were given
767+ traj_constraints = trajectory_constraints
768+ if traj_constraints is None :
769+ traj_constraints = []
770+ elif isinstance (traj_constraints , tuple ):
771+ # TODO: Check to make sure this is really a constraint
772+ traj_constraints = [traj_constraints ]
773+ elif not isinstance (traj_constraints , list ):
774+ raise TypeError ("trajectory constraints must be a list" )
775+
776+ # Process constraints
777+ minimize_constraints = []
778+ if len (traj_constraints ) > 0 :
779+ # Set up a nonlinear function to evaluate the constraints
780+ def traj_const (null_coeffs ):
781+ # Add this to the existing solution
782+ coeffs = alpha + N @ null_coeffs
783+
784+ # Evaluate the constraints at the listed time points
785+ values = []
786+ for i , t in enumerate (timepts ):
787+ # Calculate the states and inputs for the flat output
788+ M_t = Mt_list [i ]
789+
790+ # Compute flag at this time point
791+ zflag = (M_t @ coeffs ).reshape (sys .ninputs , - 1 )
792+
793+ # Find states and inputs at the time points
794+ states , inputs = sys .reverse (zflag , params )
795+
796+ # Evaluate the constraint function along the trajectory
797+ for type , fun , lb , ub in traj_constraints :
798+ if type == sp .optimize .LinearConstraint :
799+ # `fun` is A matrix associated with polytope...
800+ values .append (fun @ np .hstack ([states , inputs ]))
801+ elif type == sp .optimize .NonlinearConstraint :
802+ values .append (fun (states , inputs ))
803+ else :
804+ raise TypeError (
805+ "unknown constraint type %s" % type )
806+ return np .array (values ).flatten ()
807+
808+ # Store upper and lower bounds
809+ const_lb , const_ub = [], []
810+ for t in timepts :
811+ for type , fun , lb , ub in traj_constraints :
812+ const_lb .append (lb )
813+ const_ub .append (ub )
814+ const_lb = np .array (const_lb ).flatten ()
815+ const_ub = np .array (const_ub ).flatten ()
816+
817+ # Store the constraint as a nonlinear constraint
818+ minimize_constraints = [sp .optimize .NonlinearConstraint (
819+ traj_const , const_lb , const_ub )]
820+
821+ # Add initial and terminal constraints
822+ # minimize_constraints += [sp.optimize.LinearConstraint(M, Z, Z)]
823+
824+ # Process the initial condition
825+ if initial_guess is None :
826+ initial_guess = np .zeros (M_T0 .shape [1 ] - M_T0 .shape [0 ])
827+ else :
828+ raise NotImplementedError ("Initial guess not yet implemented." )
829+
830+ # Find the optimal solution
831+ res = sp .optimize .minimize (
832+ traj_cost , initial_guess , constraints = minimize_constraints ,
833+ ** minimize_kwargs )
834+ if res .success :
835+ alpha += N @ res .x
836+ else :
837+ raise RuntimeError (
838+ "Unable to solve optimal control problem\n " +
839+ "scipy.optimize.minimize returned " + res .message )
840+
841+ #
842+ # Transform the trajectory from flat outputs to states and inputs
843+ #
844+
845+ # Create a trajectory object to store the result
846+ systraj = SystemTrajectory (sys , basis , params = params )
847+
848+ # Store the flag lengths and coefficients
849+ # TODO: make this more pythonic
850+ coef_off = 0
851+ for i in range (sys .ninputs ):
852+ # Grab the coefficients corresponding to this flat output
853+ coef_len = basis .var_ncoefs (i )
854+ systraj .coeffs .append (alpha [coef_off :coef_off + coef_len ])
855+ coef_off += coef_len
856+
857+ # Keep track of the length of the flat flag for this output
858+ systraj .flaglen .append (len (zflag_T0 [i ]))
859+
860+ # Return a function that computes inputs and states as a function of time
861+ return systraj
0 commit comments