#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2018 Gao Tang <gt70@duke.edu>
#
# Distributed under terms of the MIT license.
"""
trajOptProblem.py
Class for describing the trajectory optimization problems.
"""
from __future__ import print_function, division
import numpy as np
from .trajOptBase import linearObj, linearPointObj
from .trajOptBase import linearPointConstr, linearConstr
from .trajOptBase import nonLinearPointObj, nonLinearObj
from .trajOptBase import nonLinearPointConstr, nonLinearConstr
from .trajOptBase import system, addX
from .trajOptBase import lqrObj
from . import SnoptConfig as snoptConfig, probFun, solver
from .utility import parseX, randomGenInBound, checkInBounds, interp
from scipy import sparse
from scipy.sparse import spmatrix, coo_matrix
[docs]class trajOptProblem(probFun):
"""A class for definition of trajectory optimization problem.
A general framework for using this class is to:
1. Define a class inherited from system and write dyn/Jdyn method.
2. Optionally, write desired cost function by inheriting/creating from the list of available cost functions.
3. Optionally, write desired constraint functions by inheriting from available constraints.
4. Create this class with selected system, discretization, t0, tf range, gradient option
5. Set bounds for state, control, parameters, x0 and xf
6. Add objective functions and constraints to this class
7. Call preProcess method explicitly
8. Create snoptConfig instance and choose desired options
9. Construct the solver
10. Use the solver to solve with either automatic guess or user provided guess
"""
[docs] def __init__(self, sys, N, t0, tf, gradmode=True, addx=None):
"""Initialize problem by system, discretization grid size, and allowable time
:param sys: system, describe system dynamics
:param N: int, discretization grid size, a uniform grid
:param t0: float/array like, allowable t0
:param tf: float/array like, allowable tf
:param gradmode: bool, sets if we use gradient mode.
:param addX: list of addX / one addX / None, additional optimization variables.
"""
assert isinstance(sys, system)
self.sys = sys
self.N = N
self.tf = tf
self.t0 = t0
if np.isscalar(tf):
self.fixtf = True
else:
self.fixtf = False
assert tf[0] <= tf[1]
if np.isscalar(t0):
self.fixt0 = True
else:
self.fixt0 = False
assert t0[0] <= t0[1]
self.gradmode = gradmode # this controls if __callg__ will be called
self.dimx = sys.nx
self.dimu = sys.nu
self.dimp = sys.np
self.dimpoint = sys.nx + sys.nu + sys.np
self.ubd = [None, None]
self.xbd = [None, None]
self.pbd = [None, None]
self.x0bd = [None, None]
self.xfbd = [None, None]
# lqr cost function
self.lqrObj = None
# Linear cost function
self.linearObj = [] # stores general linear cost
self.linPointObj = [] # stores linear cost imposed at a point
self.linPathObj = [] # stores Lagrange integral cost
# nonlinear cost function
self.nonLinObj = [] # stores general nonlinear cost
self.nonPointObj = [] # stores nonlinear cost imposed at a point
self.nonPathObj = [] # stores Lagrange integral cost. Includes LQR cost
# nonlinear constraints. Linear constraints are treated as nonlinear
self.pointConstr = [] # general constraint imposed at a certain point, such as initial and final point
self.pathConstr = [] # general constraint imposed everywhere such as collision avoidance
self.nonLinConstr = [] # stores general nonlinear constraint
self.linPointConstr = []
self.linPathConstr = []
self.linearConstr = []
# calculate number of variables to be optimized, time are always the last
numX = self.N * self.dimx
numU = self.N * self.dimu
numP = self.N * self.dimp
if addx is None:
self.lenAddX = 0
else:
if not isinstance(addx, list):
addx = [addx]
for tmp in addx:
assert isinstance(tmp, addX)
self.addX = addx
self.lenAddX = sum([tmp.n for tmp in addx])
numT = 2
if self.fixt0:
numT -= 1
if self.fixtf:
numT -= 1
numSol = numX + numU + numP + numT
self.numX = numX
self.numU = numU
self.numP = numP
self.numT = numT
self.numTraj = numSol
self.numSol = numSol + self.lenAddX
self.t0ind, self.tfind = self.__getTimeIndices()
[docs] def pre_process(self, *args):
"""Initialize the instances of probFun now we are ready.
Call this function after the objectives and constraints have been set appropriately.
It calculate the space required for SNOPT and allocates sparsity structure if necessary.
"""
numDyn = self.dimx * (self.N - 1) # constraints from system dynamics
numC = 0
for constr in self.pointConstr:
numC += constr.nf
for constr in self.pathConstr:
numC += self.N * constr.nf
for constr in self.nonLinConstr:
numC += constr.nf
nnonlincon = numC
for constr in self.linPointConstr:
numC += constr.A.shape[0]
for constr in self.linPathConstr:
numC += constr.A.shape[0] * self.N # this remains being argued
for constr in self.linearConstr:
numC += constr.A.shape[0]
self.numF = 1 + numDyn + numC
# time to analyze all objective functions in order to detect pattern for A, and additional variables for other nonlinear objective function
spA, addn = self.__analyzeObj(self.numSol, self.numF)
self.objaddn = addn # this is important for multiple objective function support
self.numSol += addn
self.numF += addn
probFun.__init__(self, self.numSol, self.numF) # currently we do not know G yet
self.__setAPattern(numDyn, nnonlincon, spA)
self.__setXbound()
self.__setFbound()
# detect gradient information
if self.gradmode: # in this case, we randomly generate a guess and use it to initialize everything
randX = self.randomGenX()
self.__turnOnGrad(randX)
[docs] def genGuessFromTraj(self, X=None, U=None, P=None, t0=None, tf=None, addx=None, tstamp=None, obj=None, interp_kind='linear'):
"""Generate an initial guess for the problem with user specified information.
An amazing feature is the user does not have to give a solution of exactly the same time-stamped trajectory used internally.
Interpolation approaches are used in such scenarios.
The user is not required to provide them all, although we suggest they do so.
:param X: ndarray, (x, x) each row corresponds to a state snapshot (if tstamp is None, assume equidistant grid). Column size can be dimx or dimx/sys.order
:param U: ndarray, (x, dimu) each row corresponds to a control snapshot. Similar to X but with column size equals dimu
:param P: ndarray, (x, dimp) each row corresponds to a parameter snapshot. Similar to X but with column size equals dimp
:param t0/tf: float/array-like, initial/final time. If None, we randomly generate one
:param addx: list of ndarray, guess of addx, if applicable
:param tstamp: ndarray, (x,), None if the X/U/P are provided using equidistant grid.
:param obj: ndarray, (x,) the objective part
:param interp_kind: str, interpolation type for scipy.interpolate.interp1d, can be (‘linear’, ‘nearest’, ‘zero’, ‘slinear’, ‘quadratic’, ‘cubic’)
"""
randX = 2 * np.random.random(self.numSol) - 1
Xtarget, Utarget, Ptarget = self.__parseX__(randX)
if obj is not None:
obj_ = self.__parseObj__(randX)
obj_[:] = obj
# generate t0 and tf, if applicable
if self.t0ind > 0:
if t0 is None:
randX[self.t0ind] = randomGenInBound(self.t0)
else:
randX[self.t0ind] = randomGenInBound(t0)
uset0 = randX[self.t0ind]
else:
uset0 = self.t0
if self.tfind > 0:
if tf is None:
randX[self.tfind] = randomGenInBound(self.tf)
else:
randX[self.tfind] = randomGenInBound(tf)
usetf = randX[self.tfind]
else:
usetf = self.tf
teval = np.linspace(uset0, usetf, self.N)
# interpolation for state variables
nPoint = self.N
dimx = self.dimx
if X is not None:
Xcol = X.shape[1]
if not (Xcol == dimx):
print('The column of X is not %d or %d, not use it' % (dimx))
X = None
else: # use interpolation to do it
interp(tstamp, X, teval, Xtarget, interp_kind)
if X is None:
# straight path go there
for i in range(nPoint):
Xtarget[i] = randomGenInBound(self.xbd, self.dimx)
# randomly generate x0 and xf
Xtarget[0, :dimx] = randomGenInBound(self.x0bd, self.dimx)
Xtarget[-1, :dimx] = randomGenInBound(self.xfbd, self.dimx)
# interpolation for control variable
if U is not None:
interp(tstamp, U, teval, Utarget, interp_kind)
else:
for i in range(nPoint):
Utarget[i] = randomGenInBound(self.ubd, self.dimu)
if self.numP > 0:
if P is not None:
interp(tstamp, P, teval, Ptarget, interp_kind)
else:
for i in range(nPoint):
Ptarget[i] = randomGenInBound(self.pbd, self.dimp)
# generate for
if self.lenAddX > 0:
if addx is None:
for field, addx_ in zip(self.__parseAddX__(randX), self.addX):
field[:] = randomGenInBound([addx_.lb, addx_.ub], addx_.n)
else:
for guess, field, addx_ in zip(addx, self.__parseAddX__(randX), self.addX):
field[:] = randomGenInBound(guess, addx_.n)
# I do not have to worry about objaddn since they are linear
return randX
[docs] def preProcess(self, *args):
"""Alias for pre_process"""
self.pre_process(*args)
def __analyzeObj(self, numSol, numF):
"""Analyze the objective function.
:param numSol: current estimation of free variables
:param numF: current estimation of rows of constraints
:return spA: coo sparse matrix, records first row of A, and last rows of A
:return addn: int, additional nonlinear constraint. As long as nonlinear obj exists, this is non-zero
"""
# detect how many nonlinear objective functions we have
if self.lqrObj is None:
nnlin = 0
else:
nnlin = 1
nnlin += len(self.nonLinObj) + len(self.nonPathObj) + len(self.nonPointObj)
addn = nnlin
# analyze the linear objective functions in a good way
A = np.zeros(numSol)
for obj in self.linearObj:
A[obj.A.col] += obj.A.data
for obj in self.linPointObj:
A[self.__patchCol__(obj.index, obj.A.col)] += obj.A.data
for obj in self.linPathObj: # this is not particularly useful, I have to say
for i in range(self.numPoint):
A[self.__patchCol__(i, obj.A.col)] += obj.A.data
# get sparse representation of A
nnzind = np.nonzero(A)[0]
A_ = np.zeros(2 * addn)
row_ = np.zeros(2 * addn, dtype=int)
col_ = np.zeros(2 * addn, dtype=int)
# for the addn
for i in range(addn):
A_[i] = 1
A_[addn + i] = -1
col_[i] = numSol + i
col_[addn + i] = numSol + i
row_[i] = 0
row_[addn + i] = numF + i
# concat them
catA = np.concatenate((A[nnzind], A_))
catArow = np.concatenate((np.zeros(len(nnzind), dtype=int), row_))
catAcol = np.concatenate((nnzind, col_))
spA = coo_matrix((catA, (catArow, catAcol)))
return spA, addn
def __setAPattern(self, ndyncon, nnonlincon, spA):
"""Set sparsity pattern for A. curRow is current row.
It finds sparsity pattern from defect constraints.
It also finds sparsity pattern from those linear constraints.
:param ndyncon: int, describes how many dynamics constraints we have
:param nnonlincon: int, describes how many nonlinear constraints we have
:param spA: sparse matrix, how the objective function is described linearly.
:return curRow: current row number after setting linear constraint
:return curNA: current length of A
"""
dimx, dimu, dimp, dimpoint = self.dimx, self.dimu, self.dimp, self.dimpoint
# find those linear constraints
curRow = ndyncon + nnonlincon + 1 # since first row is objective
lstCA = []
lstCArow = []
lstCAcol = []
for constr in self.linPointConstr:
lstCA.append(constr.A.data)
lstCArow.append(constr.A.row + curRow)
lstCAcol.append(self.__patchCol__(constr.index, constr.A.col)) # take care on here
curRow += constr.A.shape[0]
for constr in self.linPathConstr:
for index in range(self.N):
lstCA.append(constr.A.data)
lstCArow.append(constr.A.row + curRow)
lstCAcol.append(self.__patchCol__(index, constr.A.col))
curRow += constr.A.shape[0]
for constr in self.linearConstr:
lstCA.append(constr.A.data)
lstCArow.append(constr.A.row + curRow)
lstCAcol.append(constr.A.col)
curRow += constr.A.shape[0]
# for python3 compaticity, I extend the list
lstCA.append(spA.data)
lstCArow.append(spA.row)
lstCAcol.append(spA.col)
self.Aval = np.concatenate(lstCA)
self.Arow = np.concatenate(lstCArow)
self.Acol = np.concatenate(lstCAcol)
self.set_a_by_triplet(self.Aval, self.Arow, self.Acol)
curNA = len(self.Aval) # this is just for bookkeeping
return curRow, curNA
[docs] def randomGenX(self):
"""A more reansonable approach to generate random guess for the problem.
It considers bounds on initial and final states so this is satisfied.
Then it linearly interpolate between states.
Controls are randomly generated within control bound, if it presents. Otherwise [-1, 1]
:return x: ndarray, (numSol, ) an initial guess of the solution
"""
randX = np.zeros(self.numSol)
X = np.reshape(randX[:self.numX], (self.N, self.dimx))
U = np.reshape(randX[self.numX: self.numX + self.numU], (self.N, self.dimu))
# randomly generate x0 and xf
X[0] = randomGenInBound(self.x0bd, self.dimx)
X[-1] = randomGenInBound(self.xfbd, self.dimx)
# straight path go there
for i in range(self.dimx):
X[:, i] = np.linspace(X[0, i], X[-1, i], self.N)
for i in range(self.N):
U[i] = randomGenInBound(self.ubd, self.dimu)
if self.numP > 0:
P = np.reshape(randX[self.numX + self.numU: self.numX + self.numU + self.numP], (self.N, self.dimp))
for i in range(self.N):
P[i] = randomGenInBound(self.pbd, self.dimp)
if self.t0ind > 0:
randX[self.t0ind] = randomGenInBound(self.t0)
if self.tfind > 0:
randX[self.tfind] = randomGenInBound(self.tf)
if self.lenAddX > 0:
for field, addx in zip(self.__parseAddX__(randX), self.addX):
field[:] = randomGenInBound([addx.lb, addx.ub], addx.n)
# I do not have to worry about objaddn since they are linear
return randX
def __turnOnGrad(self, x0):
"""Turn on gradient, this is called after an initial x0 has been generated"""
self.grad = True
self.__getSparsity(x0)
def __getSparsity(self, x0):
"""Detect sparsity of the problem with an initial guess."""
numObjG = self.__getObjSparsity(x0)
self.numObjG = numObjG
# summarize number of pure linear constraints
numDynG = self.__getDynSparsity(x0)
numCG = 0 # G from C
# I only care about those in numC
for constr in self.pointConstr:
numCG += constr.nG
for constr in self.pathConstr:
numCG += self.N * constr.nG
for constr in self.nonLinConstr:
numCG += constr.nG
numG = numObjG + numDynG + numCG
self.numG = numG
self.nG = numG
def __getDynSparsity(self, x):
"""Set sparsity of the problem caused by system dynamics and other nonlinear constraints.
:param x: ndarray, the guess/sol
"""
# TODO: now sparsity is supported for Euler/ BackEuler/ Dis, RK4 needs to be done
h, useT = self.__getTimeGrid(x)
useX, useU, useP = self.__parseX__(x)
# use one time/state/ctrl/para/h set to detect the sparsity pattern
ydot, Jac = self.sys.Jdyn(useT[0], useX[0], useU[0], useP[0])
nrow, ncol = Jac.shape
if isinstance(Jac, np.ndarray):
nnz = nrow * ncol
self.dynSparse = False
elif isinstance(Jac, spmatrix):
nnz = Jac.nnz # TODO: this is dangerous since nnz might be misleading
self.dynSparse = True
if not self.dynSparse:
dynG = (self.N - 1) * self.dimx * (self.dimpoint + 1 + self.numT) # G from dyn
return dynG
else: # if sparse, then treat dis and Euler separately
if self.sys.ode == 'Dis':
timennz = Jac.getcol(0).nnz + Jac.getcol(-1).nnz
dynnnz = nnz - timennz
dynG = (self.N - 1) * (dynnnz + self.dimx + self.numT)
else:
timennz = Jac.getcol(0).nnz # TODO: this is in fact wrong
eyemat = sparse.diags(np.ones(self.dimx), offsets=1, shape=Jac.shape)
Jac = Jac.tocoo()
if self.sys.ode == 'Euler':
Jac.data *= h
# add a block by identity
Jac = Jac + eyemat
elif self.sys.ode == 'BackEuler':
Jac.data *= (-h)
Jac = Jac + eyemat
dynnnz = Jac.nnz - timennz
dynG = (self.N - 1) * (dynnnz + self.dimx + self.numT * self.dimx)
return dynG
def __getObjSparsity(self, x):
"""Set sparsity structure of the problem from objective function.
The objective function pattern is composed of two parts:
- linear parts. We sum all the coefficients and find sparsity pattern out of it
- nonlinear parts. Each nonlinear objective is augmented with another row in jacobian
and a another auxiliary optimization variable s.t. c(x) = y and J += y
:param x: ndarray, the guess/sol
:returns: nG: int, # Jacobian from nonlinear objective function
"""
h, useT = self.__getTimeGrid(x)
useX, useU, useP = self.__parseX__(x)
nG = 0
# check sparseObj mode
if self.lqrObj is not None:
nG += self.LQRnG
for obj in self.nonLinObj:
nG += obj.nG
for obj in self.nonPointObj:
nG += obj.nG
for obj in self.nonPathObj:
nG += (self.N - 1) * obj.nG
return nG
def __getTimeIndices(self):
"""Utility function for assigning sparsity structure."""
t0ind = -1
tfind = -1
lenX = self.N * self.dimpoint
if self.fixt0:
if self.fixtf:
pass
else:
tfind = lenX
else:
if self.fixtf:
t0ind = lenX
else:
t0ind = lenX
tfind = lenX + 1
return t0ind, tfind
def __setXbound(self):
"""Set bounds on decision variables."""
# create bound on x
dimpnt = self.dimpoint
dimx, dimu, dimp = self.dimx, self.dimu, self.dimp
xlb = np.zeros(self.numSol)
xub = np.zeros(self.numSol)
numDynVar = self.N * dimpnt
Mxlb = np.reshape(xlb[:numDynVar], (self.N, dimpnt))
Mxub = np.reshape(xub[:numDynVar], (self.N, dimpnt))
Mulb = Mxlb[:, dimx:dimx+dimu]
Muub = Mxub[:, dimx:dimx+dimu]
Mplb = Mxlb[:, dimpnt-dimp:dimpnt]
Mpub = Mxub[:, dimpnt-dimp:dimpnt]
# set bounds for q and dq, agree with previous convention
if self.xbd[0] is not None:
Mxlb[:, :dimx] = self.xbd[0]
else:
Mxlb[:, :dimx] = -1e20
# set lb for x0 and xf
if self.x0bd[0] is not None:
Mxlb[0, :dimx] = self.x0bd[0]
if self.xfbd[0] is not None:
Mxlb[-1, :dimx] = self.xfbd[0]
if self.xbd[1] is not None:
Mxub[:, :dimx] = self.xbd[1]
else:
Mxub[:, :dimx] = 1e20
# set ub for x0 and xf
if self.x0bd[1] is not None:
Mxub[0, :dimx] = self.x0bd[1]
if self.xfbd[1] is not None:
Mxub[-1, :dimx] = self.xfbd[1]
# set bounds for control variable
if self.ubd[0] is not None:
Mulb[:] = self.ubd[0]
else:
Mulb[:] = -1e20
if self.ubd[1] is not None:
Muub[:] = self.ubd[1]
else:
Muub[:] = 1e20
if self.pbd[0] is not None and self.dimp > 0:
Mplb[:] = self.pbd[0]
else:
Mplb[:] = -1e20
if self.pbd[1] is not None and self.dimp > 0:
Mpub[:] = self.pbd[1]
else:
Mpub[:] = 1e20
# set bound on time
if not self.fixt0:
xlb[self.t0ind] = self.t0[0]
xub[self.t0ind] = self.t0[1]
if not self.fixtf:
xlb[self.tfind] = self.tf[0]
xub[self.tfind] = self.tf[1]
# set bound on addX
if self.lenAddX != 0:
curN = self.numTraj
for addx in self.addX:
xlb[curN: curN + addx.n] = addx.lb
xub[curN: curN + addx.n] = addx.ub
# set bound on objaddn, this is obvious
xlb[-self.objaddn:] = -1e20
xub[-self.objaddn:] = 1e20
# assign to where it should belong to
self.set_xlb(xlb)
self.set_xub(xub)
def __setFbound(self):
"""Set bound on F"""
# set bound on F
numF = self.numF
numDyn = self.dimx * (self.N - 1) # constraints from system dynamics
clb = np.zeros(numF)
cub = np.zeros(numF)
clb[0] = -1e20
cub[0] = 1e20
cind0 = 1 + numDyn
for constr in self.pointConstr:
if constr.lb is not None:
clb[cind0: cind0 + constr.nf] = constr.lb
if constr.ub is not None:
cub[cind0: cind0 + constr.nf] = constr.ub
cind0 += constr.nf
for constr in self.pathConstr:
tmplb = np.reshape(clb[cind0: cind0 + constr.nf * self.N], (self.N, constr.nf))
tmpub = np.reshape(cub[cind0: cind0 + constr.nf * self.N], (self.N, constr.nf))
cind0 += constr.nf * self.N
if constr.lb is not None:
tmplb[:] = constr.lb
if constr.ub is not None:
tmpub[:] = constr.ub
for constr in self.nonLinConstr:
if constr.lb is not None:
clb[cind0: cind0 + constr.nf] = constr.lb
if constr.ub is not None:
cub[cind0: cind0 + constr.nf] = constr.ub
cind0 += constr.nf
# the rest are linear constraints and we should write those bounds, too
for constr in self.linPointConstr:
cindf = cind0 + constr.A.shape[0]
clb[cind0: cindf] = constr.lb
cub[cind0: cindf] = constr.ub
cind0 = cindf
for constr in self.linPathConstr:
cindf = cind0 + self.N * constr.A.shape[0]
clb[cind0: cindf] = np.tile(constr.lb, (self.N, 1)).flatten()
cub[cind0: cindf] = np.tile(constr.ub, (self.N, 1)).flatten()
cind0 = cindf
for constr in self.linearConstr:
cindf = cind0 + constr.A.shape[0]
clb[cind0: cindf] = constr.lb
cub[cind0: cindf] = constr.ub
cind0 = cindf
# the bounds for objaddn is 0 so we are good so far
# assign to where it should belong to
self.lb = clb
self.ub = cub
def __getTimeGrid(self, x):
"""Based on initial guess x, get the time grid for discretization.
:param x: ndarray, the guess/sol.
:returns: h: float, grid size
:returns: useT: the grid being used
"""
if self.fixt0:
uset0 = self.t0
else:
uset0 = x[self.t0ind]
if self.fixtf:
usetf = self.tf
else:
usetf = x[self.tfind]
h = (1.0 * (usetf - uset0)) / (self.N - 1)
useT = np.linspace(uset0, usetf, self.N)
return h, useT
def __parseX__(self, x):
"""Parse guess/sol into X, U, P"""
n_var_pnt = self.dimx + self.dimu + self.dimp
X = np.reshape(x[:self.N * n_var_pnt], (self.N, n_var_pnt))
useX = X[:, :self.dimx]
useU = X[:, self.dimx:self.dimpoint - self.dimp]
useP = X[:, self.dimpoint - self.dimp:]
return useX, useU, useP
[docs] def parseF(self, guess):
"""Give an guess, evaluate it and parse into parts.
:param guess: ndarray, (numSol, ) a guess or a solution to check
:returns: dict, containing objective and parsed constraints
"""
assert len(guess) == self.numSol
N = self.N
dimx = self.dimx
y = np.ones(self.numF)
if self.gradmode:
self.__callg__(guess, y, np.zeros(1), np.zeros(1), np.zeros(1), False, False)
else:
self.__callf__(guess, y)
obj = y[0]
dynCon = np.reshape(y[1:(N-1)*dimx+1], (N - 1, dimx))
curN = 1 + (N - 1) * dimx
pointCon = []
for constr in self.pointConstr:
pointCon.append(y[curN: curN + constr.nf])
curN += constr.nf
pathCon = []
for constr in self.pathConstr:
pathCon.append(np.reshape(y[curN: curN+N*constr.nf], (N, constr.nf)))
curN += N*constr.nf
nonLinCon = []
for constr in self.nonLinConstr:
nonLinCon.append(y[curN: curN+constr.nf])
curN += constr.nf
# check bounds, return a -1, 1 value for non-equality bounds, and 0 for equality bounds
useX, useU, useP = self.__parseX__(guess)
Xbound = checkInBounds(useX, self.xbd)
x0bound = checkInBounds(useX[0], self.x0bd)
xfbound = checkInBounds(useX[-1], self.xfbd)
ubound = checkInBounds(useU, self.ubd)
if self.dimp > 0:
pbound = checkInBounds(useP, self.pbd)
else:
pbound = None
if self.t0ind > 0:
t0bound = checkInBounds(guess[self.t0ind], self.t0)
else:
t0bound = None
if self.tfind > 0:
tfbound = checkInBounds(guess[self.tfind], self.tf)
else:
tfbound = None
if self.lenAddX > 0:
addx = self.__parseAddX__(guess)
addXbound = [checkInBounds(addx_, [addx__.lb, addx__.ub]) for addx_, addx__ in zip(addx, self.addX)]
else:
addXbound = None
return {'obj': obj, 'dyn': dynCon, 'point': pointCon, 'path': pathCon, 'nonlin': nonLinCon,
'Xbd': Xbound, 'Ubd': ubound, 'x0bd': x0bound, 'xfbd': xfbound, 'Pbd': pbound,
't0bd': t0bound, 'tfbd': tfbound, 'addXbd': addXbound}
[docs] def parse_sol(self, sol):
"""Call parseX function from utility and return a dict of solution."""
X, U, P = self.__parseX__(sol)
if self.dimp == 0:
P = None
h, tgrid = self.__get_time_grid__(sol)
obj = self.__parseObj__(sol)
if self.lenAddX == 0:
return {'t': tgrid, 'x': X, 'u': U, 'p': P, 'obj': obj}
else:
return {'t': tgrid, 'x': X, 'u': U, 'p': P, 'addx': self.__parseAddX__(sol), 'obj': obj}
def __get_time_grid__(self, x):
"""Based on initial guess x, get the time grid for discretization.
:param x: ndarray, the guess/sol.
:returns: h: float, grid size
:returns: useT: the grid being used
"""
if self.fixt0:
uset0 = self.t0
else:
uset0 = x[self.t0ind]
if self.fixtf:
usetf = self.tf
else:
usetf = x[self.tfind]
h = (usetf - uset0) / (self.N - 1)
useT = np.linspace(uset0, usetf, self.N)
return h, useT
def __parseObj__(self, x):
return x[self.numSol - self.objaddn:]
def __parseAddX__(self, x):
numTraj = self.numTraj
addX = []
for addx in self.addX:
addX.append(x[numTraj: numTraj + addx.n])
numTraj += addx.n
return addX
[docs] def __callf__(self, x, y): # TODO: remove callf case
"""Evaluate those constraints and objective functions."""
h, useT = self.__getTimeGrid(x)
useX, useU, useP = self.__parseX__(x)
# evaluate objective function
self.__objModeF__(0, h, useT, useX, useU, useP, x, y)
# evaluate the system dynamics constraint
curRow = 1
curRow = self.__dynconstrModeF__(curRow, h, useT, useX, useU, useP, y)
# evaluate other constraints
curRow = self.__constrModeF__(curRow, h, useT, useX, useU, useP, x, y)
return curRow
def __objModeF__(self, curRow, h, useT, useX, useU, useP, x, y):
"""Calculate objective function. F mode
:param curRow: int, index from which we write on
:param h, useT, useX, useU, useP: parsed solution
:param x: ndarray, the sol, it is used for linear constraints
:param y: ndarray, the F to be written. The first row stores the objective function
"""
y[0] = 0.0
tmpout = np.zeros(1)
for obj in self.linearObj:
y[0] += obj.A.dot(x)
for obj in self.linPointObj:
tmpx = np.concatenate(([useT[obj.index]], useX[obj.index], useU[obj.index], useP[obj.index]))
y[0] += obj.A.dot(tmpx)
for obj in self.nonLinObj:
obj.__callf__(x, tmpout)
y[0] += tmpout[0]
for obj in self.linPathObj:
for i in range(self.N - 1):
tmpx = np.concatenate(([useT[i]], useX[i], useU[i], useP[i]))
obj.__callf__(tmpx, tmpout)
y[0] += tmpout[0] * h
for obj in self.nonPointObj:
tmpx = np.concatenate(([useT[obj.index]], useX[obj.index], useU[obj.index], useP[obj.index]))
obj.__callf__(tmpx, tmpout)
y[0] += tmpout[0]
for obj in self.nonPathObj:
for i in range(self.N - 1):
tmpx = np.concatenate(([useT[i]], useX[i], useU[i], useP[i]))
obj.__callf__(tmpx, tmpout)
y[0] += tmpout[0] * h
for obj in self.nonLinObj:
obj.__callf__(x, tmpout)
y[0] += tmpout[0]
# add lqr cost, if applicable
if self.lqrObj is not None:
y[0] += self.lqrObj(h, useX, useU, useP)
def __constrModeF__(self, curRow, h, useT, useX, useU, useP, x, y):
"""Calculate constraint function. F mode
:param curRow: int, index from which we write on
:param h, useT, useX, useU, useP: parsed solution
:param y: ndarray, the F to be written
:returns: curRow: current row after we write on y
"""
for constr in self.pointConstr:
tmpx = np.concatenate(([useT[constr.index]], useX[constr.index], useU[constr.index], useP[constr.index]))
constr.__evalf__(tmpx, y[curRow: curRow + constr.nf])
curRow += constr.nf
for constr in self.pathConstr:
for i in range(self.N):
tmpx = np.concatenate(([useT[i]], useX[i], useU[i], useP[i]))
constr.__evalf__(tmpx, y[curRow: curRow + constr.nf])
self.numFcurRow += constr.nf
for constr in self.nonLinConstr:
constr.__evalf__(x, y[curRow: curRow + constr.nf])
curRow += constr.nf
return curRow
def __dynconstrModeF__(self, curRow, h, useT, useX, useU, useP, y):
"""Calculate constraint from dynamics. F mode.
:param curRow: int, index from which we write on
:param h, useT, useX, useU, useP: the parsed sol
:param y: ndarray, the F
:returns: curRow: current row after we write on y
"""
# loop over all system dynamics constraint
cDyn = np.reshape(y[curRow:curRow+(self.N - 1) * self.dimx], (self.N - 1, self.dimx))
for i in range(self.N - 1):
# evaluate gradient of system dynamics
ydot = self.sys.dyn(useT[i], useX[i], useU[i], useP[i])
cDyn[i] = useX[i] + h * ydot - useX[i + 1]
return curRow
[docs] def __callg__(self, x, y, G, row, col, rec, needg):
"""Evaluate those constraints, objective functions, and constraints. It simultaneously allocates sparsity matrix.
:param x: ndarray, the solution to the problem
:param y: ndarray, return F
:param G, row, col: ndarray, information of gradient
:param rec, needg: if we record/ if we need gradient
"""
h, useT = self.__getTimeGrid(x)
useX, useU, useP = self.__parseX__(x)
# loop over all system dynamics constraint
curRow = 1
curNg = 0
curRow, curNg = self.__dynconstrModeG(curRow, curNg, h, useT, useX, useU, useP, y, G, row, col, rec, needg)
curRow, curNg = self.__constrModeG(curRow, curNg, h, useT, useX, useU, useP, x, y, G, row, col, rec, needg)
curRow, curNg = self.__objModeG(curRow, curNg, h, useT, useX, useU, useP, x, y, G, row, col, rec, needg)
return curRow, curNg
def __dynconstrModeG(self, curRow, curNg, h, useT, useX, useU, useP, y, G, row, col, rec, needg):
"""Evaluate the constraints imposed by system dynamics"""
dimx, dimu, dimp = self.dimx, self.dimu, self.dimp
numX, numU = self.numX, self.numU
cDyn = np.reshape(y[curRow:curRow+(self.N - 1) * self.dimx], (self.N - 1, self.dimx))
for i in range(self.N - 1):
# evaluate gradient of system dynamics TODO: support other types of integration scheme
ydot, Jac = self.sys.Jdyn(useT[i], useX[i], useU[i], useP[i]) # TODO: support in-place Jacobian
cDyn[i] = useX[i] + h * ydot - useX[i + 1]
if needg:
if not self.dynSparse:
Jac *= h # always useful
# assign a block for x
G[curNg: curNg + dimx*dimx] = (Jac[:, 1:1+dimx] + np.eye(dimx)).flatten()
if rec:
tmpMat = np.tile(np.arange(dimx), (dimx, 1))
row[curNg: curNg + dimx*dimx] = curRow + tmpMat.T.flatten()
col[curNg: curNg + dimx*dimx] = i * dimx + tmpMat.flatten()
curNg += dimx * dimx
# assign a block for u
G[curNg: curNg + dimx*dimu] = Jac[:, 1+dimx:1+dimx+dimu].flatten()
if rec:
row[curNg: curNg + dimx*dimu] = curRow + np.tile(np.arange(dimx), (dimu, 1)).T.flatten()
col[curNg: curNg + dimx*dimu] = numX + i * dimu + np.tile(np.arange(dimu), dimx).flatten()
curNg += dimx * dimu
# assign a block for p, if necessary
if dimp > 0:
G[curNg: curNg + dimx*dimp] = Jac[:, 1+dimx+dimu:1+dimx+dimu+dimp].flatten()
if rec:
row[curNg: curNg + dimx*dimp] = curRow + np.tile(np.arange(dimx), (dimp, 1)).T.flatten()
col[curNg: curNg + dimx*dimp] = numX + numU + i * dimp + np.tile(np.arange(dimp), dimx).flatten()
curNg += dimx * dimp
# assign the diagonal block for x_{k+1}
G[curNg: curNg + dimx] = -1.0
if rec:
row[curNg: curNg + dimx] = curRow + np.arange(dimx)
col[curNg: curNg + dimx] = np.arange((i+1)*dimx, (i+2)*dimx)
curNg += dimx
# assign a column for t0, if necessary
if self.t0ind > 0:
G[curNg: curNg + dimx] = -ydot / (self.N - 1) + Jac[:, 0] * (1-float(i)/(self.N - 1.0))
if rec:
row[curNg: curNg + dimx] = curRow + np.arange(dimx)
col[curNg: curNg + dimx] = self.t0ind
curNg += dimx
# assign a column for tf, if necessary
if self.tfind > 0:
G[curNg: curNg + dimx] = ydot / (self.N - 1) + Jac[:, 0] * (float(i)/(self.N - 1.0))
if rec:
row[curNg: curNg + dimx] = curRow + np.arange(dimx)
col[curNg: curNg + dimx] = self.tfind
curNg += dimx
else:
Jac.data *= h # as always, no damage to this
eyemat = sparse.diags(np.ones(self.dimx), offsets=1, shape=Jac.shape)
Jac += eyemat # TODO: this assume forward Euler, backward needs update
dynnnz = Jac.nnz - Jac.getcol(0).nnz # TODO: this might cause serious issues since it changes structure
# convention is to assign all but first column, then assign first column
if not sparse.isspmatrix_coo(Jac):
Jac = Jac.tocoo()
rows = Jac.row
cols = Jac.col
data = Jac.data
timeone = cols == 0
nontime = np.logical_not(timeone)
G[curNg: curNg + dynnnz] = data[nontime]
if rec:
row[curNg: curNg + dynnnz] = curRow + rows[nontime]
col[curNg: curNg + dynnnz] = self.__patchCol__(i, cols[nontime])
curNg += dynnnz
# for x_{k+1}
G[curNg: curNg + dimx] = -1.0
if rec:
row[curNg: curNg + dimx] = curRow + np.arange(dimx)
col[curNg: curNg + dimx] = np.arange(dimx) + (i + 1) * self.dimpoint
curNg += dimx
if self.t0ind > 0:
G[curNg: curNg + dimx] = -ydot/(self.N - 1) + Jac.getcol(0).toarray()[:, 0] * (1 - float(i)/(self.N - 1.0))
if rec:
row[curNg: curNg + dimx] = curRow + np.arange(dimx)
col[curNg: curNg + dimx] = self.t0ind
curNg += dimx
if self.tfind > 0:
G[curNg: curNg + dimx] = ydot/(self.N - 1) + Jac.getcol(0).toarray()[:, 0] * (float(i)/(self.N - 1.0))
if rec:
row[curNg: curNg + dimx] = curRow + np.arange(dimx)
col[curNg: curNg + dimx] = self.tfind
curNg += dimx
curRow += dimx
return curRow, curNg
def __objModeG(self, curRow, curNg, h, useT, useX, useU, useP, x, y, G, row, col, rec, needg, first_row=False):
"""Calculate objective function. It just evaluates them and assign to correct position in y.
See __constr_mode_g__ for arguments and output.
"""
tmpout = np.zeros(1)
y[0] = 0
curRow = self.numF - self.objaddn
if self.lqrObj is not None: # the lqr obj
Gpiece = G[curNg: curNg + self.LQRnG]
rowpiece = row[curNg: curNg + self.LQRnG]
colpiece = col[curNg: curNg + self.LQRnG]
self.lqrObj(h, useX, useU, useP, tmpout, Gpiece, rowpiece, colpiece, rec, needg)
y[curRow] = tmpout[0]
if rec:
rowpiece[:] = curRow
curNg += self.LQRnG
curRow += 1
# still in the point, path, obj order
if len(self.nonPointObj) > 0:
for obj in self.nonPointObj:
tmpx = np.concatenate(([useT[obj.index]], useX[obj.index], useU[obj.index], useP[obj.index]))
Gpiece = G[curNg: curNg + obj.nG]
rowpiece = row[curNg: curNg + obj.nG]
colpiece = col[curNg: curNg + obj.nG]
obj.__callg__(tmpx, tmpout, Gpiece, rowpiece, colpiece, rec, needg)
y[curRow] = tmpout[0]
if rec:
rowpiece[:] = curRow
colpiece[:] = self.__patchCol__(obj.index, colpiece)
curNg += obj.nG
curRow += 1
if len(self.nonPathObj) > 0:
for obj in self.nonPathObj:
y[curRow] = 0
for i in range(self.N - 1):
tmpx = np.concatenate(([useT[i]], useX[i], useU[i], useP[i]))
Gpiece = G[curNg: curNg + obj.nG]
rowpiece = row[curNg: curNg + obj.nG]
colpiece = col[curNg: curNg + obj.nG]
obj.__callg__(tmpx, tmpout, Gpiece, rowpiece, colpiece, rec, needg)
y[curRow] += tmpout[0] * h
Gpiece[:] *= h
if rec:
rowpiece[:] = curRow
colpiece[:] = self.__patchCol__(i, colpiece)
curNg += obj.nG
curRow += 1
if(len(self.nonLinObj)) > 0:
for obj in self.nonLinObj: # nonlinear cost function
Gpiece = G[curNg: curNg + obj.nG]
rowpiece = row[curNg: curNg + obj.nG]
colpiece = col[curNg: curNg + obj.nG]
obj.__callg__(x, tmpout, Gpiece, rowpiece, colpiece, rec, needg)
y[curRow] = tmpout[0]
if rec:
rowpiece[:] = curRow
curNg += obj.nG
curRow += 1
return curRow, curNg
def __constrModeG(self, curRow, curNg, h, useT, useX, useU, useP, x, y, G, row, col, rec, needg):
"""Calculate constraint function. G mode
:param curRow: int, index from which we write on
:param curNg: int, current index in G
:param h, useT, useX, useU, useP: parsed solution
:param y: ndarray, the F to be written
:param G, row, col: ndarray, the G to be written and the locations
:param rec: bool, if we record row and col
:param needg: bool, if we need gradient information
:return curRow: current row after we write on y
:return curNg: current index in G after this
"""
# loop over other constraints
if len(self.pointConstr) > 0:
for constr in self.pointConstr:
tmpx = np.concatenate(([useT[constr.index]], useX[constr.index], useU[constr.index], useP[constr.index]))
pieceG = G[curNg: curNg + constr.nG]
pieceRow = row[curNg: curNg + constr.nG]
pieceCol = col[curNg: curNg + constr.nG]
constr.__callg__(tmpx, y[curRow: curRow + constr.nf], pieceG, pieceRow, pieceCol, rec, needg)
if rec:
pieceRow += curRow
pieceCol[:] = self.__patchCol__(constr.index, pieceCol)
curRow += constr.nf
curNg += constr.nG
if len(self.pathConstr) > 0:
for constr in self.pathConstr:
for i in range(self.N):
tmpx = np.concatenate(([useT[i]], useX[i], useU[i], useP[i]))
pieceG = G[curNg: curNg + constr.nG]
pieceRow = row[curNg: curNg + constr.nG]
pieceCol = col[curNg: curNg + constr.nG]
constr.__callg__(tmpx, y[curRow: curRow + constr.nf], pieceG, pieceRow, pieceCol, rec, needg)
if rec:
pieceRow += curRow
pieceCol[:] = self.__patchCol__(i, pieceCol)
curRow += constr.nf
curNg += constr.nG
if len(self.nonLinConstr) > 0:
for constr in self.nonLinConstr:
pieceG = G[curNg: curNg + constr.nG]
pieceRow = row[curNg: curNg + constr.nG]
pieceCol = col[curNg: curNg + constr.nG]
constr.__callg__(x, y[curRow: curRow + constr.nf], pieceG, pieceRow, pieceCol, rec, needg)
if rec:
pieceRow += curRow
curRow += constr.nf
curNg += constr.nG
return curRow, curNg
# interface functions for ipopt
[docs] def ipEvalF(self, x):
"""The eval_f function required by ipopt.
:param x: a guess/solution of the problem
:return f: float, objective function
"""
y = np.zeros(1)
h, useT = self.__getTimeGrid(x)
useX, useU, useP = self.__parseX__(x)
self.__objModeF__(0, h, useT, useX, useU, useP, x, y)
return y[0]
def __cost__(self, x):
print("Enter cost")
return self.ipEvalF(x)
[docs] def ipEvalGradF(self, x):
"""Evaluation of the gradient of objective function.
:param x: guess/solution to the problem
:return grad: gradient of objective function w.r.t x
"""
tmpG = np.zeros(self.numSol)
mask = self.Arow == 0
tmpG[self.Acol[mask]] = self.Aval[mask]
return tmpG
def __gradient__(self, x, g):
print("Enter gradient")
mask = self.Arow == 0
g[self.Acol[mask]] = self.Aval[mask]
return True
[docs] def ipEvalG(self, x):
"""Evaluation of the constraint function.
:param x: ndarray, guess/solution to the problem.
:return g: constraint function
"""
y = np.zeros(self.numF)
if self.gradmode:
G = np.zeros(1)
row = np.zeros(1, dtype=int)
col = np.zeros(1, dtype=int)
self.__callg__(x, y, G, row, col, False, False)
return y
else:
self.__callf__(x, y)
return y
def __constraint__(self, x, f):
print("Enter constraint")
G = np.zeros(1)
row = np.zeros(1, dtype=int)
col = np.zeros(1, dtype=int)
self.__callg__(x, f, G, row, col, False, False)
return 0
[docs] def ipEvalJacG(self, x, flag):
"""Evaluate jacobian of constraints. I simply call __callg__
:param x: ndarray, guess / solution to the problem
:param flag: bool, True return row/col, False return values
"""
y = np.zeros(self.numF)
G = np.zeros(self.nG)
if flag:
row = np.ones(self.nG, dtype=int)
col = np.ones(self.nG, dtype=int)
self.__callg__(x, y, G, row, col, True, True)
return row, col
else:
row = np.ones(1, dtype=int)
col = np.ones(1, dtype=int)
self.__callg__(x, y, G, row, col, False, True)
return G
def __jacobian__(self, x, g, row, col, rec):
print("Enter Jacobian")
y = np.zeros(self.numF)
self.__callg__(x, y, g, row, col, rec, True)
return 1
def __patchCol__(self, index, col, col_offset=0):
"""Find which indices it belongs to the original one for a local matrix at col"""
col = col[col > 0]
if index < 0:
index = index + self.N
return col - 1 + col_offset + index * self.dimpoint
[docs] def parseSol(self, sol):
"""Call parseX function from utility and return a dict of solution."""
return parseX(sol, self.N, self.dimx, self.dimu, self.dimp, self.t0, self.tf, True)
[docs] def addLQRObj(self, lqrobj):
"""Add a lqr objective function to the problem. It changes lqrObj into a function being called in two modes...
:param lqrobj: a lqrObj class.
"""
Fcol = lqrobj.F.col
Qcol = lqrobj.Q.col
Rcol = lqrobj.R.col
useF = len(Fcol)
useQ = len(Qcol)
useR = len(Rcol)
if lqrobj.P is not None:
Pcol = lqrobj.P.col
useP = len(Pcol)
else:
useP = 0
Pcol = []
if lqrobj.tfweight is not None:
tfweight = lqrobj.tfweight
else:
tfweight = 0.0
self.LQRnG = (self.N - 1) * (useQ + useR + useP) + useF + self.numT
num1 = (self.N - 1) * (useQ + useR + useP)
baseCol = self.dimpoint * np.arange(self.N - 1)[:, np.newaxis] # a nPoint by 1 column matrix
def __callf__(h, useX, useU, useP_):
"""Calculate the lqr cost.
:param h: float, grid size
:param useX: ndarray, parsed X
:param useU: ndarray, parsed U
:param useP: ndarray, parsed P, might be empty
"""
y = 0
if useF > 0:
y += np.sum(lqrobj.F.data * ((useX[-1, Fcol] - lqrobj.xfbase[Fcol]) ** 2))
if useQ > 0:
y += np.sum(lqrobj.Q.data * np.sum((useX[:-1, Qcol] - lqrobj.xbase[Qcol]) ** 2, axis=0)) * h
if useR > 0:
y += np.sum(lqrobj.R.data * np.sum((useU[:-1, Rcol] - lqrobj.ubase[Rcol]) ** 2, axis=0)) * h
if useP > 0:
y += np.sum(lqrobj.P.data * np.sum((useP_[:-1, Pcol] - lqrobj.pbase[Pcol]) ** 2, axis=0)) * h
y += tfweight * (h * (self.N - 1))
return y
def __callg__(h, useX, useU, useP_, y, G, row, col, rec, needg):
"""Calculate the lqr cost with gradient information.
:param h: float, grid size
:param useX, useU, useP: ndarray, parsed X, U, P from x, the same with __call__F
:param y: ndarray, a location to write the objective function onto
:param G, row, col: the gradient information
:param rec: if we record row and col
:param needg: if we need gradient information.
"""
if rec:
row[:] = 0
col_ = np.reshape(col[:num1], (self.N - 1, -1))
if not needg:
y[0] = __callf__(h, useX, useU, useP_)
else:
yF = 0.0
yQ = 0.0
yR = 0.0
yP = 0.0
yTf = tfweight * (h * (self.N - 1))
curG = 0
G_ = G[:num1].reshape((self.N - 1, -1))
if useQ > 0:
yQ = np.sum(lqrobj.Q.data * np.sum((useX[:-1, Qcol] - lqrobj.xbase[Qcol]) ** 2, axis=0)) * h
G_[:, :useQ] = 2 * h * ((useX[:-1, Qcol] - lqrobj.xbase[Qcol]) * lqrobj.Q.data)
if rec:
col_[:, :useQ] = Qcol + baseCol
curG += (self.N - 1) * useQ
if useR > 0:
yR = np.sum(lqrobj.R.data * np.sum((useU[:-1, Rcol] - lqrobj.ubase[Rcol]) ** 2, axis=0)) * h
G_[:, useQ: useQ + useR] = 2 * h * ((useU[:-1, Rcol] - lqrobj.ubase[Rcol]) * lqrobj.R.data)
if rec:
col_[:, useQ: useQ + useR] = Rcol + baseCol + self.dimx
curG += (self.N - 1) * useR
if useP > 0:
yP = np.sum(lqrobj.P.data * np.sum((useP_[:-1, Pcol] - lqrobj.pbase[Pcol]) ** 2, axis=0)) * h
G_[:, useQ + useR: useQ + useR + useP] = 2 * h * (useP_[:-1, Pcol] - lqrobj.pbase[Pcol]) * lqrobj.P.data
if rec:
col_[:, useQ + useR: useQ + useR + useP] = Pcol + baseCol + self.dimx + self.dimu
curG += (self.N - 1) * useP
if useF > 0:
yF = np.sum(lqrobj.F.data * ((useX[-1, Fcol] - lqrobj.xfbase[Fcol]) ** 2))
G[curG: curG + useF] = 2 * lqrobj.F.data * (useX[-1, Fcol] - lqrobj.xfbase[Fcol])
if rec:
col[curG: curG + useF] = np.arange(self.numX - self.dimx, self.numX)
curG += useF
if self.t0ind > 0:
G[curG] = -(yQ + yR + yP) / h / (self.N - 1) - tfweight
if rec:
col[curG: curG + 1] = self.t0ind
curG += 1
if self.tfind > 0:
G[curG] = (yQ + yR + yP) / h / (self.N - 1) + tfweight
if rec:
col[curG: curG + 1] = self.tfind
curG += 1
y[0] = yF + yQ + yR + yP + yTf
if self.gradmode:
self.lqrObj = __callg__
else:
self.lqrObj = __callf__
[docs] def addLinearObj(self, linObj):
"""Add linear objective function.
:param linObj: linearObj class
"""
assert isinstance(linObj, linearObj)
self.linearObj.append(linObj)
[docs] def addLinearPointObj(self, linPointObj, path=False):
"""Add linear point objective function.
:param linPointObj: linearPointObj class
:param path: bool, if this is path obj (at every point except for final one)
"""
assert isinstance(linPointObj, linearPointObj)
if path:
self.linPathObj.append(linPointObj)
else:
self.linPointObj.append(linPointObj)
[docs] def addNonLinearObj(self, nonlinObj):
"""Add nonlinear objective function.
:param nonLinObj: a nonLinObj class
"""
assert isinstance(nonlinObj, nonLinearObj)
self.nonLinObj.append(nonlinObj)
[docs] def addNonLinearPointObj(self, nonPntObj, path=False):
"""Add nonlinear point objective.
:param nonPntObj: nonLinObj class
:param path: bool, if this obj is pointwise
"""
assert isinstance(nonPntObj, nonLinearPointObj)
if path:
self.nonPathObj.append(nonPntObj)
else:
self.nonPointObj.append(nonPntObj)
[docs] def addNonLinearPointConstr(self, pntConstr, path=False):
"""Add point constraint.
:param pntConstr: pointConstr class
:param path: bool, if this obj
"""
assert isinstance(pntConstr, nonLinearPointConstr)
if path:
self.pathConstr.append(pntConstr)
else:
self.pointConstr.append(pntConstr)
[docs] def addNonLinearConstr(self, constr):
"""Add a general nonlinear constraint.
:param constr: nonLinConstr class
"""
assert isinstance(constr, nonLinearConstr)
self.nonLinConstr.append(constr)
[docs] def addLinearConstr(self, constr):
assert isinstance(constr, linearConstr)
self.linearConstr.append(constr)
[docs] def addLinearPointConstr(self, constr, path=False):
assert isinstance(constr, linearPointConstr)
if path:
self.linPathConstr.append(constr)
else:
self.linPointConstr.append(constr)
[docs] def addObj(self, obj, path=False):
"""A high level function that add objective function of any kind."""
if isinstance(obj, linearObj):
self.addLinearObj(obj)
elif isinstance(obj, linearPointObj):
self.addLinearPointObj(obj, path)
elif isinstance(obj, nonLinearObj):
self.addNonLinearObj(obj)
elif isinstance(obj, nonLinearPointObj):
self.addNonLinearPointObj(obj, path)
elif isinstance(obj, lqrObj):
self.addLQRObj(obj)
[docs] def addConstr(self, constr, path=False):
if isinstance(constr, linearConstr):
self.addLinearConstr(constr)
elif isinstance(constr, linearPointConstr):
self.addLinearPointConstr(constr, path)
elif isinstance(constr, nonLinearConstr):
self.addNonLinearConstr(constr)
elif isinstance(constr, nonLinearPointConstr):
self.addNonLinearPointConstr(constr, path)
[docs] def setN(self, N):
"""Set N.
:param N: the size of discretization.
"""
self.N = N
[docs] def sett0tf(self, t0, tf):
"""Set t0 and tf.
:param t0: float/ndarray (2,) allowable t0
:param tf: float/ndarray (2,) allowable tf
"""
self.t0 = t0
self.tf = tf
if np.isscalar(tf):
self.fixtf = True
else:
self.fixtf = False
assert tf[0] <= tf[1]
if np.isscalar(t0):
self.fixt0 = True
else:
self.fixt0 = False
assert t0[0] <= t0[1]