Package: pmpc
pmpcPython-interface Particle Sequential Convex Programming Model Predictive Control (SCP PMPC) interface.This is non-linear dynamics finite horizon MPC solver with consensus
optimization capability, support for arbitrary constraints and arbitrary cost.Table of ContentspmpcTable of ContentsQuick StartInstallationInstallation from Source(Optional) Obtaining (a dynamically linked version of) PythonCompilation Times and a Persistent Solver ProcessObtaining JuliaBasic UsageDefining dynamicsDefining CostsolveMethod Arguments GlossarySolver HyperparametersSolver SettingsAdditional Dynamics SettingsNonlinear Cost and ConstraintsVariable LayoutMisc SettingsAdvanced UsageMultiple solver processesConsensus Optimization for Control under UncertaintyNon-convex Cost ExampleArbitrary ConstraintsLinear ConstraintsSecond-order Cone Constraints (SOCP)Exponential Cone ConstraintsSolver selectionParticle (consensus/contingency) optimizationWarm-start supportCiting This WorkQuick StartTake a look atexamples/simple_demo.ipynb.InstallationWe offer precompiled binaries for Linux and MacOS (x86_64 and aarch64) viaPyPI.$pipinstallpmpcEven precompiled binaries have some startup time, so time only the second run of the solver.Installation from SourceInstallation can be done by cloning this repository and issuingpip install .$gitclonehttps://github.com/StanfordASL/pmpc.git
$cdpmpc
$pipinstall.Werequirethatyou must havejuliain your systemPATHJulia version is1.6+Wehighly recommendthatobtain a dynamically linked Pythonwhich will make thejuliaPython interface startup much fasterobtain Julia 1.9this version introduces avastlyfaster time-to-first-run via better compilation utilitiesFurther subsections explain someoptionalinstallation steps.(Optional) Obtaining (a dynamically linked version of) PythonThe Python module usespyjuliato be able
to call Julia from Python which has a limitation in that it works much better
(faster startup time) witha Python version which is not statically linked to
libpython. A great workaround, obtaining a python version that is not
statically linked can be easily done viapyenv.Instructions on how to build a python version dynamically linked to libpython can be foundinpyjuliadocumentationhereor inpyenvdocumentationhereWith a working version ofpyenv, an example might be$envPYTHON_CONFIGURE_OPTS="--enable-shared"pyenvinstall3.9.13
$pyenvvirtualenv3.9.13{yourvenvname}$pyenvactivate{yourvenvname}Compilation Times and a Persistent Solver ProcessA large downside of using a Julia for the core solver is that compilation needs
to occur every time a new process is launched. This is exacerbated bypyjuliawhich does not work well with
not-dynamically linked python interpreters (most commonly used).To overcome compilation times, we can start a solver process once and the library can
use that solver process repeatedly, even through script restarts.Use$python3-mpmpc.remoteto start a persistent solver process.Next, from your script>>>frompmpc.remoteimportsolve# instead of `from pmpc import solve`Obtaining JuliaYou can downloadJulia, the programming language and interpreter, fromhere.Make sure Julia is in yourPATH.Basic UsageThe solver is capable of MPC consensus optimization for several system instantiations. For the basic usage, we'll focus on a single system MPC.A basic MPC problem is defined using the dynamics and a quadratic costDefining dynamicsx0the initial state of shapewherenp.shape(x0)==(xdim,)f, fx, fu = f_fx_fu_fn(xt, ut)an affine dynamics linearization, such that
$$x^{(i+1)} \approx f^{(i)} + f_x^{(i)} (x^{(i)} - \tilde{x}^{(i)}) + f_u^{(i)} (u^{(i)} - \tilde{u}^{(i)}) $$
wherenp.shape(xt)==(N,xdim)np.shape(ut)==(N,udim)np.shape(f)==(N,xdim)np.shape(fx)==(N,xdim,xdim)np.shape(fu)==(N,xdim,udim)Defining CostX_ref, Qa reference and quadratic weight matrix for state costU_ref, Ra reference and quadratic weight matrix for control costThe cost is given as$$J = \sum_{i=0}^N \frac{1}{2} (x^{(i+1)} - x_\text{ref}^{(i+1)}) Q^{(i)} (x^{(i+1)} - x_\text{ref}^{(i+1)}) + \frac{1}{2} (u^{(i)} - u_\text{ref}^{(i)}) R^{(i)} (u^{(i)} - u_\text{ref}^{(i)})$$Note: Initial state, x0, is assumed constant and thus does not feature in the cost.Note: When handling controls, we'll always havenp.shape(U) == (N, udim)Note: When handling states, we'll have eithernp.shape(X) == (N + 1, xdim)withx0included at the beginning ornp.shape(X) == (N, xdim)withx0NOT INCLUDED.X[:, -1]always refers to the state N, whereasU[:, -1]always refers to control N - 1.Thus, an example call would be>>>importpmpc>>>X,U,debug=pmpc.solve(f_fx_fu_fn,Q,R,x0,X_ref,U_ref)>>>help(pmpc.solve)Take a look attests/simple.pyfor simple usagetests/dubins_car.pyfor defining dynamicssolveMethod Arguments GlossarySolver HyperparametersThe solver has two scalar hyperparamters, the dynamics linearization deviation penalty for states and controls$$J_\text{deviation} = \sum_{i=0}^N \frac{1}{2} \rho_x (x^{(i+1)} - x_\text{prev}^{(i+1)})^T (x^{(i+1)} - x_\text{prev}^{(i+1)}) + \rho_u (u^{(i)} - u_\text{prev}^{(i)})^T (u^{(i)} - u_\text{prev}^{(i)})$$reg_x- state deviation in-between SCP iterations regularizationreg_u- control deviation in-between SCP iterations regularizationHigher values will slow evolution between SCP iterations and will require more
SCP iterations to converge to a solution, but will avoid instability in the
solution if the dynamics are not sufficiently smooth.Solver Settingsverbose- whether to print iteration status (user-facing)debug- whether to print very low-level debugging information (developer-facing)max_it- maximum number of SCP iterations to perform (can be fewer if tolerance met earlier)time_limit- the time limit in seconds for SCP iterationres_tol- deviation tolerance past which solution is accepted (measure of convergence)slew_rate- the quadratic penalty between time-consecutive controls (encourages smooth controls)u_slew- the previous action taken to align the first plan action with (useful for smooth receding horizon control)Additional Dynamics SettingsX_prev- previous state solution (guess), $x^{(i)} ~~ \forall i \in [1, \dots, N]$,shape = (N, xdim)U_prev- previous control solution (guess),  $u^{(i)} ~~ \forall i \in [0, \dots, N - 1]$,shape = (N, udim)x_l- state lower box constraints, $x^{(i)} ~~ \forall i \in [1, \dots, N]$,shape = (N, xdim)x_u- state upper box constraints, $x^{(i)} ~~ \forall i \in [1, \dots, N]$,shape = (N, xdim)u_l- control lower box constraints, $u^{(i)} ~~ \forall i \in [0, \dots, N - 1]$,shape = (N, udim)u_u- control upper box constraints, $u^{(i)} ~~ \forall i \in [0, \dots, N - 1]$,shape = (N, udim)Nonlinear Cost and ConstraintsThe solver supports custom arbitrary cost via each-SCP-iteration cost linearization and custom constraints via each-SCP-iteration constraint reformulation into any convex-cone constraint.lin_cost_fnis an optional callable which allows specifying a custom cost, it
should take arbitraryX,Uand return a tuplecx, the linearization of the cost with respect to the state,np.shape(cx) == (N, xdim) or cx is Nonecu, the linearization of the cost with respect to the controls,np.shape(cu) == (N, udim) or cu is NoneI highly recommend using an auto-diff library to produce the linearizations to avoid unnecessary bugs.extra_cstrs_fnsis an optional callable which returnsa listof conic constraints given arbitraryX, Ua conic constraint $G z - h \in \mathcal{K}$, (e.g., $A z - b \leq 0$) consists of a tuple of 8 elementsl- the number of non-positive orthant constraints (an integer)q- a list with the sizes of second order cone constraints (list of integers)e- the number of exponential cone constraints (integer)G_left- the G matrix referring to existing variablesG_right- the G matrix for new variables to introduceh- the right hand side vector for the constraintc_left- the additive linear cost augmentation for existing variablesc_right- the linear (minimization) cost for new variables to introduceNote:lin_cost_fnis expected to return a tuple, butextra_cstrs_fnsmust be a list of functions!Variable LayoutThe variable layout is control variables (# = N udim) followed by state variables (# = N xdim).For consensus optimization the layout isconsensus controls (# = Nc udim)free controls (# = M (N - Nc) udim)states (# = M N xdim)Misc Settingssolver_settings- a dictionary of settings to pass to the lower-level Julia solversolver_state- a previous solver state to pass to the lower-level Julia solverfilter_method- whether to apply SCP solution low-pass-like filtering to avoid SCP solution oscillationfilter_window- how large a SCP iteration window to pick for SCP solution filteringfilter_it0- what iteration of SCP to start applying filtering fromreturn_min_viol- whether to return the minimum deviation solution (measured from previous), not the last onemin_viol_it0- what iteration to start looking for minimum deviation solutionAdvanced UsageMultiple solver processesLaunching multiple solver processes can be useful when many problems need to be solved. This presents some challengesthe solver needs to be able to discover existing, ready persistent worker processeswe useredisherebatch of problem specifications need to be fed to a solution functionwe redefine the optimal control problem slightly as a set of keyword arguments for thesolvefunction, positional arguments are now keywords argumentsLaunch$python3-mpmpc.remote--help

usage:remote.py[-h][--portPORT][--verbose][--worker-numWORKER_NUM][--redis-hostREDIS_HOST][--redis-portREDIS_PORT][--redis-passwordREDIS_PASSWORD]optionalarguments:-h,--helpshowthishelpmessageandexit--portPORT,-pPORTTCPportonwhichtostarttheserver--verbose,-v--worker-numWORKER_NUM,-nWORKER_NUMNumberofworkerstostart,0meansnumberequaltophysicalCPUcores.--redis-hostREDIS_HOSTRedishost--redis-portREDIS_PORTRedisport--redis-passwordREDIS_PASSWORDRedispassword

$python3-mpmpc.remote--worker-num0--redis-passwordmy_redis_passwordthenfrompmpc.remoteimportsolve_problemsproblems=[generate_problem(...)for_inrange(32)]solutions=solve_problems(problems)We solve the discovery problem by making use ofredis, an in-memory key-value store (a database).You need to have a workingredis-serverrunning(e.g.,sudo apt install redis-server).Note: Since our worker processes are TCP based, they can be run on any network connected computer (which has the copy of the python environment). As long as we can ensure that all computers can access a redis database where workers register.We solve the problem specification by redefining the optimal control problem slightly. All arguments to thesolvefunction are now keyword argumentsa problem is defined as a dictionaryDict[str, Any].Thesolve_problemsmethod solves a list of dictionary problem definitions and returns a list of solutions.Consensus Optimization for Control under UncertaintyFor consensus control, simplybatch the system instantiations into an extra (first) dimensionall problem data now obtains a sizeMfirst dimensionsspecifyNcas a field insolver_settings, the length of the consensus horizonTODO more detailed explanationNon-convex Cost ExampleN,xdim,udim=10,3,2X_ref=np.random.rand(N,xdim)deflin_cost_fn(X,U):# cost is np.sum((X - X_ref) ** 2)cx=2*(X-X_ref)cu=Nonereturncx,cuArbitrary ConstraintsArbitrary convex (cone) constraints can be introduced in a canonical form via a callaback which recomputes them at every SCP iteration. This allows to encode non-convex constraints via their SCP convexification.The canonical form for a convex problem is given by$$
\begin{aligned}
& \text{minimize} && c^T z \
& \text{such that} && A_i z \leq_{\mathcal{K}} b_i ~~ \forall i
\end{aligned}
$$where $A z \leq_{\mathcal{K}} b$ refers to a cone constraint. The three supported cone constraints areLinear ConstraintsSimply $A z \leq b$Second-order Cone Constraints (SOCP)Mathematically
$||A_{2:n} z - b_{2:n}||_2 \leq a_1^T z - b_1$
or in codenp.linalg.norm(A[1:,:]@z-b[1:])<=A[0,:].T@z-b[0]Exponential Cone ConstraintsThe exponential cone is defined as a 3 output matrix expression (the image of
the cone matrix is of dimension 3). We follow the convention fromJuMP.jl$$
K_\text{exp} = \lbrace (x, y, z) \in \mathbb{R}^3 ~~~~ : ~~~~ y e^{x / y} \leq z, ~~~~ y \geq 0 \rbrace
$$for$$A v - b = (x, y, z)$$so $A \in \mathbb{R}^{n \times 3}$ and $b \in \mathbb{R}^3$.Solver selectionThe underlying convex solver can be selected by passing a composite keyword argumentsol=solve(...,solver_settings=dict(solver="ecos")# for example, or "cosmo", "osqp", "mosek")ECOS- FREE - very fast and very general (used by default)OSQP- FREE - very fast, but only supports linear constraintsCOSMO- FREE - very general, but it tends to run very slowlyMosek- NOT FREE - extremely fast, but requires a (not free) licenserequires: a Mosek licenseParticle (consensus/contingency) optimizationTODOWarm-start supportWarm-startin SCP MPC can refer to eitherwarm-starting the SCP procedure through a goodX_prev, U_prev- this is supportedwarm-starting the underlying convex solver - not supportedWarm-starting of the SCP procedure by providing a goodX_prev, U_prevguess is supported and very much encouraged for good SCP performance!Warm-starting of the underlying convex solver is currently not supported, as it does not lead to a noticeable
performance improvement on problems we tested the solver on.Citing This Work@inproceedings{dyro2021particle,
  author = {Dyro, Robert and Harrison, James and Sharma, Apoorva and Pavone, M.},
  title = {{P}article {M}{P}{C} for {U}ncertain and {L}earning-{B}ased {C}ontrol},
  booktitle = {{I}{E}{E}{E}/{R}{J}{S} {I}nternational {C}onference on {I}ntelligent {R}{O}bots and {S}ystems},
  year = {2021},
}