Skip to content

Commit 78f7bbb

Browse files
committed
initial implementation of solve_flat_ocp
1 parent 0ab0d07 commit 78f7bbb

3 files changed

Lines changed: 411 additions & 5 deletions

File tree

control/flatsys/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,4 @@
6262
from .linflat import LinearFlatSystem
6363

6464
# Package functions
65-
from .flatsys import point_to_point
65+
from .flatsys import point_to_point, solve_flat_ocp

control/flatsys/flatsys.py

Lines changed: 312 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)