Goal
Our goal was to create the movement of a front kick that a human could produce. We started off getting a double pendulum in a Python Template to replicate the general motion of the dynamic movement.
Outcomes

If we could have done it better another time, we would have made the motion more realistic/natural. We attempted to do that with minimal PID Control by adjusting the double pendulum .py file where we adjusted the Dampening . We added keyframes that are specific to our motion but we did not figure out a way to increase the number of keyframes in the loop.
Code
dblpend_keyframes.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | #!/usr/bin/env python3 """Prototype Python 3 script for the 16-375 double-pendulum control exercise.""" ################################################################ # Written in 2018-2019 by Garth Zeglin <garthz@cmu.edu> # To the extent possible under law, the author has dedicated all copyright # and related and neighboring rights to this software to the public domain # worldwide. This software is distributed without any warranty. # You should have received a copy of the CC0 Public Domain Dedication along with this software. # If not, see <http://creativecommons.org/publicdomain/zero/1.0/>. ################################################################ import math import numpy as np import rcp.doublependulum from rcp.ex.dblpend import main ################################################################ class PendulumController(rcp.doublependulum.DoublePendulumController): def __init__( self ): super ().__init__() # for this example, there are four fixed poses commanded a fixed intervals, expressed as a set of (q0, q1) pairs self .keyframes = np.zeros(( 4 , 2 )) self .last_frame = None return #================================================================ def setup( self ): self .write( """Keyframed position control demonstration. Applies a sequence of joint targets to a position controller at fixed intervals. The parameter sliders 0 and 1 control the 'shoulder' and 'elbow' angles for the first keyframe, 2 and 3 are the second keyframe, etc. """ ) return #================================================================ def user_parameter_change( self , parameter, value): pose = parameter / / 2 joint = parameter % 2 if pose < len ( self .keyframes): self .keyframes[pose][joint] = 2 * math.pi * (value - 0.5 ) self .write( "Set pose %d joint %d to angle %f" % (pose, joint, self .keyframes[pose][joint])) def apply_configuration( self , config): self .keyframes[ 0 ,:] = [ - 1.42 , - 1.95 ] self .keyframes[ 1 ,:] = [ 2.5 , - 0.3 ] self .keyframes[ 2 ,:] = [ 0.625 , - 0.36 ] self .keyframes[ 3 ,:] = [ - 0.1 , - 0.07 ] return # if 'keyframes' in config: # for i in range(len(self.keyframes)): # q0 = config['keyframes'].getfloat('keyframe-%d-q0' % i, fallback = 0.0) # q1 = config['keyframes'].getfloat('keyframe-%d-q1' % i, fallback = 0.0) # self.keyframes[i,:] = [q0, q1] # return def gather_configuration( self , config): if 'keyframes' not in config: config[ 'keyframes' ] = {} for i in range ( len ( self .keyframes)): config[ 'keyframes' ][ 'keyframe-%d-q0' % i] = str ( self .keyframes[i, 0 ]) config[ 'keyframes' ][ 'keyframe-%d-q1' % i] = str ( self .keyframes[i, 1 ]) return #================================================================ def compute_control( self , t, dt, state, tau): """Method called from simulator to calculate the next step of applied torques. :param t: time in seconds since simulation began :param state: four element numpy array with joint positions ``q`` and joint velocities ``qd`` as ``[q0, q1, qd0, qd1]``, expressed in radians and radians/sec :param tau: two element numpy array to fill in with the joint torques to apply ``[tau0, tau1]`` """ # select the current keyframe based on the time frame = int (t / / 1.5 ) if frame ! = self .last_frame: self .write( "Starting frame %d" % frame) self .last_frame = frame # select the pose for the current keyframe, looping over the available poses pose = self .keyframes[frame % len ( self .keyframes)] # create a target state by extending the pose to include velocity target = np.concatenate((pose, np.zeros( 2 ))) # calculate position and velocity error as difference from reference state qerr = target - state # apply PD control to reach the pose (no integral term) tau[ 0 ] = ( self .kp[ 0 ] * qerr[ 0 ]) + ( self .kd[ 0 ] * qerr[ 2 ]) tau[ 1 ] = ( self .kp[ 1 ] * qerr[ 1 ]) + ( self .kd[ 1 ] * qerr[ 3 ]) return ################################################################$ if _name_ = = "_main_" : main(PendulumController) |
doublependulum.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | # Standard library modules. import math, time, threading, queue, logging # Third-party library modules. import numpy as np # set up logger for module log = logging.getLogger(_file_) ################################################################ class DoublePendulumController( object ): """Prototype for a double-pendulum controller. This class is typically subclassed to customize the control strategy and the subclass passed into the common application framework. :ivar initial_state: four-element numpy array used to initialize the simulator state: [q1, q2, qd1, qd2] :ivar kp: two-element numpy array with default proportional position gains :ivar ki: two-element numpy array with default integral gains :ivar kd: two-element numpy array with default velocity damping gain """ def __init__( self ): # delegate object to which debugging and status messages can be printed; it must implement write() self .console = None # fixed controller parameters self .initial_state = np.array([ 0.0 , 0.0 , 0.0 , 0.0 ]) self .d_state = np.array(( 1.0 , 0.5 * math.pi, 0.0 , 0.0 )) self .kp = np.array(( 16.0 , 8.0 )) self .ki = np.array(( 4.0 , 2.0 )) self .kd = np.array(( 10.0 , 2.0 )) return #================================================================ def connect_console( self , console): """Attach a console object to be used for debugging output. The object needs to implement write().""" self .console = console return def write( self , string): """Write a message to the debugging console. If console is not available, writes to the log as an info message.""" if self .console is not None : self .console.write(string) else : log.info(string) return def setup( self ): """Hook for final one-time object configuration. This is called once prior to the start of the simulation. The default implementation does nothing.""" pass def user_parameter_change( self , parameter, value): """Hook for interactive parameter changes (e.g. GUI sliders). The default implementation does nothing. :param parameter: integer index of parameter, starting with zero :param value: dimensionless parameter value, ranges from zero to one inclusive. """ pass #================================================================ def apply_configuration( self , config): """Hook for applying parameters saved in a configuration file. The default implementation does nothing. :param config: configparser object (which implements the mapping protocol) """ pass def gather_configuration( self , config): """Hook for saving parameters in a configuration file. The default implementation does nothing. :param config: configparser object (which implements the mapping protocol) """ pass #================================================================ def compute_control( self , t, dt, state, tau): """Method called from numerical pendulum simulation to calculate the next step of applied torques. This is usually overridden in a subclass, but the default implementation applies a fixed-target PD position control. :param t: time in seconds since simulation began :param state: four element ndarray of joint positions q and joint velocities qd as [q1, q2, qd1, qd2], expressed in radians and radians/sec :param tau: two element ndarray to fill in with joint torques to apply """ # convenience variables to notate the state variables q1 = state[ 0 ] # 'shoulder' angle in radians q2 = state[ 1 ] # 'elbow' angle in radians qd1 = state[ 2 ] # 'shoulder' velocity in radians/second qd2 = state[ 3 ] # 'elbow' velocity in radians/second # calculate position and velocity error as difference from reference state qerr = self .d_state - state # apply PD control to reach the pose (no integral term) tau[ 0 ] = ( self .kp[ 0 ] * qerr[ 0 ]) + ( self .kd[ 0 ] * qerr[ 2 ]) tau[ 1 ] = ( self .kp[ 1 ] * qerr[ 1 ]) + ( self .kd[ 1 ] * qerr[ 3 ]) return ################################################################ class DoublePendulumSimulator( object ): """Numerical dynamic simulation of a frictionless double pendulum. It communicates with a user-supplied control object to compute applied joint torques. """ def __init__( self ): # the object used to calculate joint torques self .control = None # set default dynamics self .set_default_dynamic_parameters() # configure transient state self .reset() return def connect_controller( self , controller): """Attach a controller object used to compute joint torques and set the initial state.""" self .control = controller self .reset() return def reset( self ): """Reset or initialize all simulator state variables.""" self .t = 0.0 self .dt = 0.001 if self .control is not None : self .state = self .control.initial_state.copy() else : self .state = np.array([ 0.0 , 0.0 , 0.0 , 0.0 ]) self .tau = np.array([ 0.0 , 0.0 ]) self .dydt = np.ndarray(( 4 ,)) return def set_default_dynamic_parameters( self ): """Set the default dynamics coefficients defining the rigid-body model physics.""" self .l1 = 1.0 # proximal link length, link1 self .l2 = 1.0 # distal link length, link2 self .lc1 = 0.5 # distance from proximal joint to link1 COM self .lc2 = 0.5 # distance from distal joint to link2 COM self .m1 = 1.0 # link1 mass self .m2 = 1.0 # link2 mass self .I1 = ( self .m1 * self .l1 * * 2 ) / 12 # link1 moment of inertia self .I2 = ( self .m2 * self .l2 * * 2 ) / 12 # link2 moment of inertia self .gravity = - 9.81 return #================================================================ def deriv( self ): """Calculate the accelerations for a rigid body double-pendulum dynamics model. :returns: system derivative vector as a numpy ndarray """ q1 = self .state[ 0 ] q2 = self .state[ 1 ] qd1 = self .state[ 2 ] qd2 = self .state[ 3 ] LC1 = self .lc1 LC2 = self .lc2 L1 = self .l1 M1 = self .m1 M2 = self .m2 d11 = M1 * LC1 * LC1 + M2 * (L1 * L1 + LC2 * LC2 + 2 * L1 * LC2 * math.cos(q2)) + self .I1 + self .I2 d12 = M2 * (LC2 * LC2 + L1 * LC2 * math.cos(q2)) + self .I2 d21 = d12 d22 = M2 * LC2 * LC2 + self .I2 h1 = - M2 * L1 * LC2 * math.sin(q2) * qd2 * qd2 - 2 * M2 * L1 * LC2 * math.sin(q2) * qd2 * qd1 h2 = M2 * L1 * LC2 * math.sin(q2) * qd1 * qd1 phi1 = - M2 * LC2 * self .gravity * math.sin(q1 + q2) - (M1 * LC1 + M2 * L1) * self .gravity * math.sin(q1) phi2 = - M2 * LC2 * self .gravity * math.sin(q1 + q2) # now solve the equations for qdd: # d11 qdd1 + d12 qdd2 + h1 + phi1 = tau1 # d21 qdd1 + d22 qdd2 + h2 + phi2 = tau2 rhs1 = self .tau[ 0 ] - h1 - phi1 rhs2 = self .tau[ 1 ] - h2 - phi2 # Apply Cramer's Rule to compute the accelerations using # determinants by solving D qdd = rhs. First compute the # denominator as the determinant of D: denom = (d11 * d22) - (d21 * d12) # the derivative of the position is trivially the current velocity self .dydt[ 0 ] = qd1 self .dydt[ 1 ] = qd2 # the derivative of the velocity is the acceleration. # the numerator of qdd[n] is the determinant of the matrix in # which the nth column of D is replaced by RHS self .dydt[ 2 ] = ((rhs1 * d22 ) - (rhs2 * d12)) / denom self .dydt[ 3 ] = (( d11 * rhs2) - (d21 * rhs1)) / denom return self .dydt #================================================================ def timer_tick( self , delta_t): """Run the simulation for an interval. :param delta_t: length of interval in simulated time seconds """ while delta_t > 0 : # calculate next control outputs self .control.compute_control( self .t, self .dt, self .state, self .tau) # calculate dynamics model qd = self .deriv() # Euler integration self .state = self .state + self .dt * qd delta_t - = self .dt self .t + = self .dt ################################################################ |
Comments are closed.