As you know, linearization is a very useful technique in optimal control. For example, we can linearize a bicopter about the hovering equilibrium point and use an LQR controller (based on the linearized dynamics) to fly the bicopter autonomously surprisingly well.
In this lesson, you will create a leaf system implementing the dynamics of a pendulum, and then use the auto-diff tools built into drake to linearize the pendulum about both its equilibrium points. As a word of caution, using auto-diff in Python with Drake is quite smelly, but it's worth it for how amazing the results are.
Read through the starter code and focus on what you are familiar with. You should recognize a lot of the stuff in that file from Lessons 1 and 2, but there is a lot of weird stuff going on as well.
Here is the structure of the Pendulum "class":
@TemplateSystem.define("Pendulum_")
def Pendulum_(T):
class Impl(LeafSystem_[T]):
def _construct(self, converter=None):
LeafSystem_[T].__init__(self, converter)
...
def _construct_copy(self, other, converter=None):
Impl._construct(self, converter=converter)
def DoCalcTimeDerivatives(self, context, derivatives):
...
return Impl
Pendulum = Pendulum_[None] # default instantiationwhere the ... is filled in with code you already know how to write. This structure is what allows the auto-diff to take derivatives of the dynamics with respect to inputs, outputs, and state variables. For further reading, see an official tutorial and the cpp documentation on system scalar conversion.
Use the starter code to get your matrix K. Then, in a separate file, create a simulation of a pendulum with an LQR state feedback controller which drives the system state to the origin. Make sure to start the pendulum at a non-zero initial condition, and note that all angles are in radians. The feedback control law is given by Ctrl+Shift+C).
Next, you can modify your code to make the pendulum balance at the upright equilibrium point. Simply linearize the pendulum about the top equilibrium point to get a new matrix
Add saturation to the output of the controller (saturate at +/- 5.0 N-m). Make the initial condition (0,0). Does the LQR controller work? Now set the saturation to +/- 10 N-m. Does the control work now?
Now, you will create a closed-loop system which can do swing-up control of the inverted pendulum using energy shaping. The idea is simple: you want to get the total energy of the system to be equal to
Hint: create functions
EvalPotentialEnergy(self, context)andEvalKineticEnergy(self, context)in your pendulum class which will return (not output - no need to use an output port here) the potential and kinetic energy. Your energy shaping controller can call these functions (with the correct context passed in) in itsDoOutput()function. Make sure that when the pendulum state is at the origin, the total energy is zero.
When the energy error is small and the pendulum is near the upright position, you can switch to an LQR controller. You will need to linearize the pendulum about the upright equilibrium point in order to get the correct K matrix. Also, the feedback controller should be
Note: you can either make a few difference controllers (energy shaping, LQR) and then create a third system (mux) to dynamically switch which controller output is actually going to the plant based on the energy level and plant state, or you can just define one massive controller to do everything (what I did). The choice is yours.
Warning: because the pendulum dynamics technically evolve on a circle, there can be issues with "wrapping" the angle
$$\theta$$ . We have defined the target position to be$$x^* = (\pi, 0)$$ so if the pendulum is upright at$$x = (-\pi, 0)$$ , the controller won't actually realize the pendulum is in the upright state and will apply counterclockwise torque to increase the value of$$\theta$$ . You can either account for this by wrapping$$\theta$$ to be on the interval$$[0,2\pi)$$ inside your controller, or you can just cheat and use an initial condition that just "works" by avoiding this problem (this might take a few attempts to guess good initial conditions - I found(-10 deg, 0)did the trick.). Wrapping can be accomplished withnp.mod(theta, np.pi*2).
This is a hard task, but you can find inspiration in Chapter 2 of underactuated.mit.edu and its deepnote notebook solution. If you need to peek at my solution, go ahead, but instead of copy and pasting immediately, try to read and understand my plant and controller.
Warning: make sure you set the initial condition to a non-zero value! Otherwise the energy shape control will no nothing because
$$\dot\theta(t) = 0$$ so the output torque will be zero!
Pro Tip: if you are having trouble with energy shaping, you can just set the initial condition to something like 179 degrees and see if your LQR controller can balance the pendulum upright.