+
Implementing a PID Controller

Developed by Hishem Olav Kløvnes and Tor Ole Odden - Published July 2, 2025

DOI: 10.1119/PICUP.Exercise.PID_Controller

This set of exercises guides the student through the principles and implementation of a PID (Proportional-Integral-Derivative) controller by simulating the vertical motion of a drone. The goal is to build intuition for how each component of the controller influences stability and responsiveness in a dynamic physical system. The student will begin by analyzing the forces acting on a drone in one dimension, using free body diagrams to understand equilibrium and deviations from it. The simulation starts with a simple proportional controller, then gradually incorporates derivative and integral terms. Through numerical integration using the Euler-Cromer method, the student observes how the controller affects the motion and how each term contributes to improving stability, responsiveness, and steady-state error correction. The final sections introduce random perturbations such as wind and moving targets to simulate real-world disturbances. Students are encouraged to evaluate the controller's performance under these conditions and to reflect on the physical implications. Since PID controllers are widely used in laboratory instruments and experimental setups, these exercises also provide useful context for understanding practical control systems. The exercises are designed to be completed in a Jupyter notebook, using a provided code template. The emphasis is on building intuition through hands-on simulation and interpreting physical behavior through computational modeling.
Subject Area Mechanics
Level First Year
Available Implementation IPython/Jupyter Notebook
Learning Objectives
## Leaning Objectives After completing this exercise, the student will be able to: - Create and extend simple model for a PID controller (**Exercise 1-4**) - Numerically integrate and differentiate the equations of motion to understand how a PID controller stabilizes a dynamic system (**Exercise 2-4**) - Implement proportional, integral, and derivative control terms and evaluate their effects on stability, responsiveness, and steady-state error (**Exercise 2-4**) - (Bonus) Apply PID control to a more complex system with disturbances and interpret the physical implications of controller performance (**Exercise 5**)
Time to Complete 90 min

These exercises are not tied to a specific programming language. Example implementations are provided under the Code tab, but the Exercises can be implemented in whatever platform you wish to use (e.g., Excel, Python, MATLAB, etc.).

## 1. Visualizing the system Before we can simulate the PID controller, we need to understand the forces acting on the drone. To do this, we will begin by drawing a **free body diagram** to visualize these forces. ### 1 a) Free Body Diagram Sketch the system and draw a free body diagram of a drone hovering at a target height $h$. Since we are working in one dimension (vertical), horizontal forces can be ignored. The drone is affected by two main forces: gravity, pulling it downward, and thrust, pushing it upward. The thrust force is controlled via the drone’s "gas pedal", which adjusts how much upward force is applied to counteract gravity. Draw one free body diagram for a light drone, and one for a heavy drone. What is the difference between the two? ### 1b) Change of forces The free body diagrams above describe the drone when it is hovering steadily at the target height $h$, meaning there is a balance between the forces. But what happens when the drone is not at the target height? Are the forces still the same? ## 2. The P Controller We will start by implementing the proportional (P) component of the PID controller. A proportional controller adjusts its output based on how far the drone is from the target height. Meaning, the larger the difference, the stronger the response. In our case, the controller output is the thrust force applied to the drone, which we write as a function of time $u(t)$. The error is the difference between the target height and the current height of the drone: $$ u(t) = K_P e(t) $$ where $K_P$ is the proportional gain, $e(t)$ is the error at time $t$, and $u(t)$ is the resulting thrust force. A larger error means more thrust. In this section, we will see how a proportional controller alone affects the drone's motion, and why it is often not enough on its own. ### 2 a) Finding the acceleration To simulate the drone's motion, we need to calculate the vertical acceleration. This is done by finding the net force acting on the drone, which is the difference between the thrust force and the gravitational force. The gravitational force is constant and can be represented as $F_g = m \cdot g$, where $m$ is the mass of the drone and $g$ is the acceleration due to gravity (approximately 9.81 m/s²). Using Newton's second law $F=ma$, express the drone's acceleration $a(t)$ as a function of the thrust force $u(t)$ and the gravitational force $F_g$. ### 2 b) Implementing the acceleration function Using the expression for acceleration, implement the function `acceleration(thrust, m, g)` that calculates the vertical acceleration of the drone. ``` import numpy as np import matplotlib.pyplot as plt def acceleration(thrust, m, g): """ Calculates the net vertical acceleration of the drone. """ return ... ``` ### 2 c) The proportional component Now we have implemented a function for acceleration. But as you can see, the acceleration depends on the thrust force $u(t)$, which we have not yet defined. In this subsection, we want to implement two more functions: 1. `proportional_component(K_P, error)` that calculates the thrust force $u(t)$ based on the error $e(t)$ and the proportional gain $K_P$. 2. `error(target_height, current_height)` that calculates the error $e(t)$ as the difference between the target height and the current height of the drone. These are the last two pieces we need to implement the proportional component of the controller. ``` def calculate_error(target_height, current_height): """ Calculates the difference between the target height and the current height of the drone. """ return ... def proportional_component(K_P, error): return ... ``` ### 2 d) Simulate the P controller Now that we have all the pieces, we are ready to simulate the movement of the drone using the proportional controller. In the following code section, you will implement the Euler-Cromer method to numerically integrate the velocity and position of the drone. The Euler-Cromer method is an extension of the Euler method and is significantly more stable and reliable for simulating physical systems when the acceleration is known. The Euler-Cromer equations for this system are: $$ \begin{align*} v(t + \Delta t) &= v(t) + a(t) \Delta t\\ r(t + \Delta t) &= r(t) + v(t + \Delta t) \Delta t \end{align*} $$ where $v(t)$ is the velocity at time $t$, $r(t)$ is the position at time $t$, and $\Delta t$ is the time step. ``` ## initialize the system m = 1.0 # mass target_height = ... # desired position E.g 10.0 start_position = 0.0 # initial position start_velocity = 0.0 # initial velocity g = 9.81 # gravitational acceleration K_P = ... # proportional gain n = 500 T = np.linspace(0, 20, n) # time vector dt = T[1] - T[0] # time step position = np.zeros(n) # position velocity = np.zeros(n) # velocity accelerations = np.zeros(n) # acceleration position[0] = start_position # set initial position velocity[0] = start_velocity # set initial velocity # Euler cromer method for numerical integration for i in range(1, n): # Calculate the error between the target and the current position # Calculate the control input using the proportional term function # Calculate the acceleration # Update velocity and position using Euler-Cromer method ... # plot the results plt.figure(figsize=(10, 5)) plt.plot(T, position, label='Position', color='blue') plt.axhline(target_height, color='red', linestyle='--', label=f'Setpoint({target_height} m)') plt.title('P controller: Position vs Time') plt.xlabel('Time (s)') plt.ylabel('Position (m)') plt.legend() plt.grid() plt.show() ``` ## 3) PD controller We have just witnessed the limitations of a P controller. It only reacts to the current error, meaning the difference between the target height and the measurement position, without considering how the error is changing over time. This often leads to overshooting and oscillations in the system. To improve the controller, we introduce a derivative component, D, which responds to the rate of change of the error. This allows the controller to anticipate where the system is heading, not just where it currently is. In other words, the derivative component acts as anticipatory control, pushing back harder when the error is changing rapidly and backing off when things are stabilizing. It effectively adds damping, which helps smooth out the response and prevent overshooting. Mathematically, the controller signal is now proportional to both the error and its derivative: $$u(t) = K_p e(t) + K_D \frac{\partial e(t)}{\partial t}$$ ### 3 a) Understanding the derivative component Before we implement the derivative component, let us first try to understand how it affects the system. The derivative component is the rate of change of the error, which can be approximated by: $$\frac{\partial e(t)}{\partial t} \approx \frac{e(t) - e(t - \Delta t)}{\Delta t}$$ where $\Delta t$ is the time step, $e(t)$ is the error at time $t$, and $e(t - \Delta t)$ is the error at the previous time step. Now consider these situations: 1. What happens to the thrust force $u(t)$ when the drone is **below** the target height, but rising quickly? 2. What happens to the thrust force $u(t)$ when the drone is **above** the target height, and falling back towards it? 3. What happens with the thrust force $u(t)$ when the drone is at a **stable** height (not necessarily the target height)? *Hint: Think about the sign of the error and the sign of its derivative in each case* ### 3 b) Implementing the derivative component The next thing we need to do is to implement the derivative of the error into the simulation. A simple way of calculating the derivative is to use the difference between the current and previous error: $$\frac{\partial e(t)}{\partial t} = \frac{e(t) - e(t-1)}{\Delta t}$$ Fill in the function `derivative_component(K_D, error, previous_error, dt)` below. ``` def derivative_component(K_D, current_error, previous_error, dt): """ Derivative term for a PID controller: K_D · (d e(t))/(d t). """ return ... ``` ### 3 c) Simulating the PD controller Now that you have implemented the derivative component, you are ready to simulate the movement of the drone using the PD controller. Use the Euler Cromer method to numerically integrate the velocity and position of the drone. The PD controller will use the proportional and derivative components to calculate the thrust force. Also plot the result and observe how it changes compared to the P controller. ``` K_P = ... # proportional gain K_D = ... # derivative gain position = np.zeros(n) # position velocity = np.zeros(n) # velocity accelerations = np.zeros(n) # acceleration position[0] = start_position # set initial position velocity[0] = start_velocity # set initial velocity #Euler-Cromer method for numerical integration with PD control previous_error = ... # initialize previous error for i in range(1, n): # calculate error # Calculate the control input using the proportional term and the derivative term functions # Update velocity and position using Euler-Cromer method # update previous error for next iteration ... ``` Notice how the derivative term stabilizes the drone. ## 4) PID controller We have now seen the effects of the P and D terms in the controller. While the derivative term helps by anticipating changes in error and damping the system response, the PD controller is still not perfect—particularly when it comes to eliminating steady-state errors, where the drone hovers at a height that is not exactly the desired height To address this problem, we will introduce the integral term, I. The integral term accumulates the error over time, effectively "remembering" past errors. If there is any persistent difference between the target and the measurement, the integral term will continue to grow, pushing the controller to adjust the output until the error is eliminated. This cumulative effect is what allows the controller to eliminate residual or static errors that the proportional term alone cannot handle. As the error decreases, the contribution from the proportional term naturally weakens, but the integral term, having built up over time, continues to drive the output until the error is zero. Once the error reaches zero, the integral term stops growing, stabilizing the system. Together, the three terms combine into a PID controller, which is defined as follows: $$u(t) = K_P e(t) + K_I \int_0^t e(t) dt + K_D \frac{\partial e(t)}{\partial t} $$ ### 4 a) Implementing the integral component We want to implement the integral of the error into the simulation by using a simple numerical integration method, such as a Riemann sum: $$\int_0^t e(t) dt \approx \sum_{i=0}^{n} e(t_i) \Delta t$$ The Riemann sum will be accumulated in a variable, which needs to be initialized as zero, and then updated in each iteration of the simulation loop. Fill in the function `integration_component(K_I, Riemann_sum)` below. ``` def integration_component(K_I, Riemann_sum): """ Integral term for a PID controller: K_I · Sum( e(t)·dt) """ return ... ``` ### 4 b) Simulating the PID controller Now that you have implemented all the necessary functions for the PID controller, you should be able to simulate the drone’s movement using the PID controller combined with the Euler Cromer method. ``` # Initialize the system K_P = ... # proportional gain K_D = ... # derivative gain K_I = ... # integral gain position = np.zeros(n) # position velocity = np.zeros(n) # velocity accelerations = np.zeros(n) # acceleration position[0] = start_position # set initial position velocity[0] = start_velocity # set initial velocity ``` Hopefully, you have now seen a significant improvement in the drone’s stability. Your PID controller should be able to keep the drone steady at the target height. Notice how the integral term eliminates steady-state errors, which was apparent in the PD controller. Now, try adjusting the gains $K_P$, $K_I$, and $K_D$ to see how they affect the system’s response. You can also experiment with changing the target height and observe how the controller reacts to different desired altitudes. ## 5. Add random effects In the real world, there are often random effects that can affect the stability of the drone. For example, wind gusts can push the drone up or down, and we need to adjust the thrust accordingly. In this exercise, we will add a random effect to the simulation to see how the PID controller reacts to it. We have created a few example functions to simulate the effects listed below. Try implementing them in your simulation to see how the PID controller reacts. You can also create your own random effects and observe the controller’s response. ### Examples: - Wind gusts - Air resistance - Moving target position ### 5 a) Free body diagram with random effects Before implementing the effects, first draw a free body diagram of the drone including the random effects you want to explore. How do these effects change the forces acting on the drone, and how do they affect the thrust force that the PID controller needs to apply? ### 5 b) Implementing random effects Implement the random effects you want to explore in the simulation. You can use the example functions provided or create your own. Make sure to adjust the forces acting on the drone accordingly. ``` def drag_force(v, drag_coeff): """ Calculates the drag force acting on the drone. """ return -drag_coeff * v def wind_force(wind_amplitude): """ Randomly generates a wind """ return np.random.uniform(-wind_amplitude, wind_amplitude) ## Moving setpoint def moving_target(t): """ Example function of a moving target that changes over time. """ return 10.0 + 5.0 * np.sin(1.0 * t) * np.exp(-0.09 * t) #plot the moving setpoint moving_targets = moving_target(T) plt.figure(figsize=(10, 5)) plt.plot(T, moving_targets, label='Moving Setpoint', color='green') plt.title('Moving Setpoint vs Time') plt.xlabel('Time (s)') plt.ylabel('Setpoint (m)') plt.legend() plt.grid() plt.show() ```

Download Options

Share a Variation

Did you have to edit this material to fit your needs? Share your changes by Creating a Variation

Credits and Licensing

Hishem Olav Kløvnes and Tor Ole Odden, "Implementing a PID Controller," Published in the PICUP Collection, July 2025, https://doi.org/10.1119/PICUP.Exercise.PID_Controller.

DOI: 10.1119/PICUP.Exercise.PID_Controller

The instructor materials are ©2025 Hishem Olav Kløvnes and Tor Ole Odden.

The exercises are released under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 license

Creative Commons Attribution-NonCommercial-ShareAlike 4.0 license