class DCMotorParams():
    def __init__(self, J=0.01, b=0.1, K=0.01, R=1, L=0.5):
            #     (J)     moment of inertia of the rotor     0.01 kg.m^2
            #     (b)     motor viscous friction constant    0.1 N.m.s
            #     (Ke)    electromotive force constant       0.01 V/rad/sec
            #     (Kt)    motor torque constant              0.01 N.m/Amp
            #     (R)     electric resistance                1 Ohm
            #     (L)     electric inductance                0.5 H
            #     Note that in SI units, Ke = Kt = K
        self.J = J
        self.b = b
        self.K = K
        self.R = R
        self.L = L
                
class DCMotor():
    def __init__(self, x0, params):
        self._params  = params
                
        self._x0 = x0
        self._x = x0
        self._J_load = 0
        self.update_motor_matrix()
        
    def update_motor_matrix(self):
        # state variables are: rotation speed (w, or theta_dot) and current (i)
        self._A = np.array([[-self._params.b/(self._params.J+self._J_load),  self._params.K/(self._params.J+self._J_load)],
                            [-self._params.K/self._params.L,  -self._params.R/self._params.L]])
        self._B = np.array([[0],[1/self._params.L]])
        self._C = np.array([1,   0])
        self._D = 0;
        
    def reset(self):
        self._x = self._x0
        
    def set_load(self, J_load):
        self._params.J += J_load   
        self.update_motor_matrix()
        
    def step(self, dt, u):
        self._x = self._x + dt*(self._A@self._x + self._B*u)
              
    def measure_speed(self):
        self._y = self._C@self._x 
        return self._y
motor = DCMotor(np.array([[0], [0]]), DCMotorParams())
y = []
u = 1 # input

time_vector = np.arange(0, 10, 0.01)
for t in time_vector:
    motor.step(0.01, u)
    y.append(motor.measure_speed())  
fig = plt.figure(figsize=(10,5))

plt.plot(time_vector, y, linewidth=3)
plt.xlabel('time (s)')
plt.ylabel('speed (w, rad/s)');

Now we can add a load on the motor to verify how it changes the response

motor.reset()
motor.set_load(0.1)
y = []
u = 1 # input (Volts)

time_vector = np.arange(0, 10, 0.01)
for t in time_vector:
    motor.step(0.01, u)
    y.append(motor.measure_speed())    
fig = plt.figure(figsize=(10,5))

plt.plot(time_vector, y)
plt.xlabel('time (s)')
plt.ylabel('speed (w, rad/s)');

As expected, given than now we have a higher load the motor takes more time to get to its steady state.

Extended DCMotor

The DCMotor defined in the previous class does not include the rotor's position in its state. We are going to slighlty modify the equations to add one state to explicit the rotor's position.

This is the class that we export in our library.

We also define a wrapping function to make sure the rotor position stays within $[-\pi,\pi]$.

wrap[source]

wrap(angle)

Wraps an angle between -pi and pi.

class DCMotorParams[source]

DCMotorParams(J=0.01, b=0.1, K=0.01, R=1, L=0.5)

class DCMotor[source]

DCMotor(x0, params)

motor = DCMotor(x0=np.array([[0], [0], [0]]), params=DCMotorParams())
y = np.array([[0],[0]]) # initialise to 0. This is only here to have the correct size. Alternative: y = np.empty((2,1)) 

u = 1 # input

time_vector = np.arange(0, 10, 0.01)
for t in time_vector:
    motor.step(0.01, u)
    y = np.append(y, motor.measure(), axis=1)   
    
# remove the first item which was there only to have the correct size
y = np.delete(y, 0, axis=1)

position = y[0,:]
velocity = y[1,:]
fig, axs = plt.subplots(2, 1, figsize=(10,5))

axs[0].plot(time_vector, velocity, linewidth=3)
axs[0].set_xlabel('time (s)')
axs[0].set_ylabel('speed (w, rad/s)')

axs[1].plot(time_vector, position, linewidth=3)
axs[1].set_xlabel('time (s)')
axs[1].set_ylabel('position (rad)');

plt.tight_layout()

DC Motor Transfer Function

Or if we want to express it as a transfer function: $$ P(s) = \frac{\dot\Theta(s)}{V(s)} = \frac{K}{(Js+b)(Ls+R) + K^2} $$

and if we want to measure the position

$$ \tilde{P}(s) = \frac{\Theta(s)}{V(s)} = \frac{K}{s(Js+b)(Ls+R) + K^2} $$

Adding a motor controller

The next step is to have a motor controller so that we can use it in our workspace.

class PID[source]

PID(Kp, Kd, Ki)

PID controller.

pendulum = Pendulum(theta_0=np.radians(0), 
                    theta_dot_0=0, 
                    params=PendulumParameters(b=0.2))

motor = DCMotor(x0=np.array([[0], [0], [0]]), 
                params=DCMotorParams())

pid = PID(Kp=1000, 
          Kd=500, 
          Ki=1000)

# connect the motor to the pendulum
motor.connect_to(pendulum)

t0, tf, dt = 0, 20, 0.01
time_vector = np.arange(t0, tf, dt)

angle_des = 5 # desired final value

pendulum_angles = []
for t in time_vector:  
    u = pid.control(dt, y_des=np.radians(angle_des), 
                        y=pendulum.position())
    torque = motor.step(dt, u)
    pendulum.step(dt, u=torque)
    pendulum_angles.append(np.degrees(pendulum.position()))
                                             
print('Reference position: {:.4f} rad/{:.1f} deg'.format(np.radians(angle_des), angle_des))
print('Pendulum position (tf): {:.4f} rad/{:.1f} deg'.format(np.radians(pendulum_angles[-1]), pendulum_angles[-1]))
Reference position: 0.0873 rad/5.0 deg
Pendulum position (tf): 0.0873 rad/5.0 deg
plt.plot(time_vector, pendulum_angles)
plt.plot(time_vector, angle_des*np.ones((len(pendulum_angles))));
plt.xlabel('time (s)')
plt.ylabel('pendulum position (deg)');
plt.ylim(0, angle_des+1);
plt.grid()

class Simulator[source]

Simulator(pendulum, motor, controller)

pendulum = Pendulum(theta_0=np.radians(0), 
                    theta_dot_0=0, 
                    params=PendulumParameters(b=0.2))

motor = DCMotor(x0=np.array([[0], [0], [0]]), 
                params=DCMotorParams())

pid = PID(Kp=1000, 
          Kd=500, 
          Ki=1000)

# connect the motor to the pendulum
motor.connect_to(pendulum)

t0, tf, dt = 0, 20, 0.01
sim = Simulator(pendulum, motor, pid)
sim.y_des(10) # desired final value in degrees

results = sim.run(t0, tf, dt)
fig, axs = plt.subplots(len(results.keys())-1, 1, figsize=(10,10))

for index, key in enumerate(results.keys()):
    if key == 'time (s)': continue
    if key == 'position (rad)':
        axs[index].plot(results['time (s)'], np.degrees(results[key]), linewidth=3)
        axs[index].plot(results['time (s)'], sim.y_des*np.ones(results['time (s)'].shape), color='r', linewidth=3)
    else:
        axs[index].plot(results['time (s)'], results[key], linewidth=3)
    axs[index].set_xlabel('time (s)')
    axs[index].set_ylabel(key)
    axs[index].grid()

    

plt.tight_layout()

Animate

class AnimateControlledPendulum[source]

AnimateControlledPendulum(pendulum, motor, controller, t0, tf, dt)

See also: https://jakevdp.github.io/blog/2012/08/18/matplotlib-animation-tutorial/

pendulum = Pendulum(theta_0=np.radians(0), 
                    theta_dot_0=0, 
                    params=PendulumParameters(b=0.2))

motor = DCMotor(x0=np.array([[0], [0], [0]]), 
                params=DCMotorParams())

pid = PID(Kp=1000, 
          Kd=500, 
          Ki=1000)

# connect the motor to the pendulum
motor.connect_to(pendulum)

t0, tf, dt = 0, 10, 1./30 # time
sim = Simulator(pendulum, motor, pid)

sim.y_des(10) # desired final value in degrees
results = sim.run(t0, tf, dt)

ap = AnimateControlledPendulum(sim)
ap.start_animation(t0, tf, dt)