Optimal Control Problem Tutorial

The previous section on phases covers everything you need to know about formulating and solving single phase optimal control problems. In this section, we will cover how you can link these phases together into a multi phase optimal control problem using the oc.OptimalControlProblem type.

As we walk through the OptimalControlPoblem API, we will make use of the trivial ODEs shown below.

import numpy as np
import asset_asrl as ast

vf        = ast.VectorFunctions
oc        = ast.OptimalControl
Args      = vf.Arguments

class DummyODE(oc.ODEBase):
    def __init__(self,xv,uv,pv):
        args = oc.ODEArguments(xv,uv,pv)
        super().__init__(args.XVec(),xv,uv,pv)


# 6 states indexed [0,1,2,3,4,5], time index 6, no controls or ODE params
odeX   = DummyODE(6, 0, 0)

# 6 states indexed [0,1,2,3,4,5], time index 6, 3 controls [7,8,9], no ODE params
odeXU  = DummyODE(6, 3, 0)

# 7 states indexed [0,1,2,3,4,5,6], time index 7, 3 controls indexed [8,9,10], one ODE param
odeXUP = DummyODE(7, 3, 1)

Initialization

To start, we will create 6 phases (2 of each ODE) type, as shown below that we will then link together inside of a single optimal control problem. Though not shown here, each phase may be constructed using methods we discussed in the previous section.

phase0 = odeXUP.phase("LGL3")
phase1 = odeXUP.phase("LGL3")

phase0.setStaticParams([0.0])
phase1.setStaticParams([0.0])

phase2 = odeXU.phase("LGL3")
phase3 = odeXU.phase("LGL3")

phase4 = odeX.phase("LGL3")
phase5 = odeX.phase("LGL3")

phase4.setStaticParams([0])
phase5.setStaticParams([0])

#. setup phases 0 through 5
#.
#.
#.
#.

We can then create an OptimalControlProblem (here named ocp), and add our phases using the addPhase method. The individual phases in an OptimalControlProblem MUST Be unique objects. The software will detect if you attempt to add the same phase to an OptimalControlProblem twice and throw an error. The commented out line below will throw an error because the specific phase5 object has already been added to the OptimalControlProblem.

ocp  = oc.OptimalControlProblem()

ocp.addPhase(phase0)
ocp.addPhase(phase1)
ocp.addPhase(phase2)
ocp.addPhase(phase3)
ocp.addPhase(phase4)
ocp.addPhase(phase5)

#ocp.addPhase(phase5)  #phases must be unique, adding same phase twice will throw error

You can access the phases in an OptimalControlProblem using the ocp.Phase(i) method where i is the index of the phase in the order they were added. If the phase is created elsewhere in the script you can manipulate it through that object or via the .Phase(i) method as shown below. Note, phases are large stateful objects and we do not make copies of them , thus ocp.Phase(0) and phase0 are the EXACT same object. Be careful not to apply duplicate constraints to the same phase.

ocp.Phase(0).addBoundaryValue("Front",range(0,6),np.zeros((6)))

# Equivalent to above,make sure you dont accidentally do both.
# phase0.addBoundaryValue("Front",range(0,6),np.zeros((6)))

Additionally, you may access the list of phases already added to an ocp using the .Phases field of the object. This can allow you to iterate over all phases to apply similar constraints/objectives to some or all of the phases as shown below.

for phase in ocp.Phases:
    phase.addDeltaTimeObjective(1.0)

As a general rule of thumb, any constraint or objective that can be applied to the individual phases to represent your goal, should be, and not with the OptimalControlProblem API that we are about to cover in the next section. For example, if our intent was to minimize the total time elapsed time of all of our phases, applying addDeltaTimeObjective to every phase should be preferred to an equivalent formulation using Link Objectives.

Analogous to the concept of a phase’s static parameters, you may also add additional free variables that we call “link parameters” to an ocp as shown below.

ocp.setLinkParams(np.ones((15)))

Solving and Optimizing

After constructing an OptimalControlProblem and its constituent phases, we can now use PSIOPT to solve or optimize the entire trajectory. As with phase, the settings of the optimizer can be manipulated through a reference to PSIOPT attached to the ocp object. Additionally, as before, calls to the optimizer are handled through the ocp itself as shown below. Both of these topics are handled in more details in the section on PSIOPT.

ocp.optimizer ## reference to this ocps instance of psiopt
ocp.optimizer.set_OptLSMode("L1")

## Solve just the dynamics,equality, and inequality constraints
flag = ocp.solve()

## Optimize objective subject to the dynamic,equality, and inequality constraints
flag = ocp.optimize()

## Call solve to find feasible point, then optimize objective subject to the dynamic,equality, and inequality constraints
flag = ocp.solve_optimize()

## Same as above but calls solve if the optimize call fails to fully converge
flag = ocp.solve_optimize_solve()

After finding a solution, you can retrieve the converged trajectories for each phase using either the original defined phases or ocp.Phase(i). Additionally you can retrieve the values of any link parameters using returnLinkParams.

Traj0 = ocp.Phase(0).returnTraj()
Traj0 = phase0.returnTraj()   # Remember phase0 and ocp.Phase(0) are the same object!!

StatParams1 = ocp.Phase(1).returnStaticParams()

LinkParams = ocp.returnLinkParams()

Finally, you can refine the meshes for some or all of the constituent phases and then resolve the problem.

ocp.solve_optimize()

for phase in ocp.Phases:
    phase.refineTrajManual(5000)

ocp.optimize()


Trajs = [phase.returnTraj() for phase in ocp.Phases]

CTrajs = [phase.returnCostateTraj() for phase in ocp.Phases]

Miscellaneous Topics

Referencing and Removing Constraints

When adding any of the 3 types of constraints/objectives covered previously, an integer identifier or list of integers is returned by the method. This identifier can be used to remove a constraint/objective from the optimal control problem. This can be quite useful when you want to express some homotopic or continuation scheme without having to create a new ocp at each step. Given the identifier for a function of a certain type, it can be removed from the phase using the corresponding .removeLink#####(id) method as shown below.

###########################
## Ex. Equality Constraint

eq1 = ocp.addLinkEqualCon(SomeFunc,
                    2,'Last', range(0,3),
                    3,'First',range(0,3))

ocp.removeLinkEqualCon(eq1)

###########################
## Ex. Inequality Constraint

iq1 = ocp.addLinkInequalCon(ALinkInequalCon(),
                    0,'Last', XtUvars0,[],SPvars0,
                    1,'First',XtUvars1,OPvars1,[])


ocp.removeLinkInequalCon(iq1)

###########################
## Ex.Link Objective

ob1 = ocp.addLinkObjective(ALinkObjective(),
                    0,'Last', XtUvars0,[],SPvars0,
                    1,'First',XtUvars1,OPvars1,[])

ocp.removeLinkObjective(ob1)

Retrieving Constraint Violations and Multipliers

Immediately after a call to PSIOPT, users can retrieve the constraint violations and Lagrange multipliers associated with user applied link constraints. For equality and inequality constraints, constraint violations and multipliers are retrieved by supplying a constraint function’s id to the phase’s .returnLink####Vals(id) and .returnLink####Lmults(id) methods as shown below. In all cases, the violations/multipliers are returned as a list of numpy arrays each of which contains the output/multipliers associated with each call to the function inside the optimization problem.

eq1 = ocp.addLinkEqualCon(SomeFunc,
                    2,'Last', range(0,3),
                    3,'First',range(0,3))


iq1 = ocp.addLinkInequalCon(ALinkInequalCon(),
                    0,'Last', XtUvars0,[],SPvars0,
                    1,'First',XtUvars1,OPvars1,[])

ocp.optimize()

ecvals = ocp.returnEqualConVals(eq1)
ecmults = ocp.returnEqualConLmults(eq1)

icvals  = ocp.returnInequalConVals(iq1)
icmults = ocp.returnInequalConLmults(iq1)

Additionally, the multipliers and constraint values for the phases inside of an optimal control can be retrieved as shown in the phase tutorial.