Simulating Differential Equations with Euler's Method
A simple C++ simulation showing how continuous motion can be approximated with small time steps.
Maruf Hossain4 min read
Differential equations describe change: velocity, acceleration, springs, circuits, population growth, and a lot of other systems that move over time.
In code, we usually do not simulate continuous change directly. We approximate it with small steps.
This post is about that basic idea using Euler's method.
Starting with Velocity
For a falling object, velocity changes because of gravity. If I define downward as the positive direction:
dv/dt = g
If g = 9.8 m/s², then Euler's method updates velocity like this:
v_next = v_current + g × dt
Here is a small C++ version:
#include <iostream>
#include <iomanip>
int main() {
const double g = 9.8;
const double dt = 0.1;
double v = 0.0;
double t = 0.0;
std::cout << "Time (s)\tVelocity (m/s)" << std::endl;
while (t <= 5.0) {
std::cout << std::fixed << std::setprecision(1) << t << "\t\t"
<< std::fixed << std::setprecision(2) << v << std::endl;
v= v + g * dt;
t= t + dt;
}
return 0;
}The program is not solving the equation symbolically. It is stepping through time and updating the state.
That is the part I wanted to understand.
There is a subtle detail here: because gravity is constant in this example, Euler's update gives the exact velocity at each sampled time. It is still useful for seeing the update pattern, but it does not expose much numerical error. That becomes visible once the derivative depends on the current state.
Why the Time Step Matters
Euler's method is easy to write, but the step size matters. A smaller dt usually gives a better approximation, but it also means more iterations.
That tradeoff shows up everywhere in simulation work:
- smaller steps are more accurate
- larger steps are faster
- too large a step can make the simulation unstable
This is why numerical methods are not just math formulas. They become engineering decisions.
Euler's method is a first-order method. Roughly speaking, halving the time step should halve the accumulated error over a fixed interval. The dependable way to check is to run the same simulation with dt, dt / 2, and dt / 4, then compare the outputs at the same times. If the answers are not settling toward one another, reducing the step again—or changing methods—is more useful than trusting a single smooth-looking result.
A Spring-Mass Example
A spring-mass system is more interesting because position and velocity affect each other.
The second-order equation:
d²x/dt² = -(k/m)x
can be split into:
dx/dt = v
dv/dt = -(k/m)x
Then the code updates both position and velocity each step.
#include <iostream>
#include <iomanip>
int main() {
const double k = 10.0;
const double m = 1.0;
const double dt = 0.01;
double x = 1.0;
double v = 0.0;
double t = 0.0;
std::cout << "Time (s)\tPosition (m)\tVelocity (m/s)" << std::endl;
while (t <= 10.0) {
std::cout << std::fixed << std::setprecision(2) << t << "\t\t"
<< std::fixed << std::setprecision(3) << x << "\t\t"
<< std::fixed << std::setprecision(3) << v << std::endl;
double dx_dt= v;
double dv_dt= -(k / m) * x;
x= x + dx_dt * dt;
v= v + dv_dt * dt;
t= t + dt;
}
return 0;
}This is where the idea clicked for me. The simulation is just a loop, but the loop is carrying the state of the system forward.
It also reveals a weakness of explicit Euler. For an undamped spring, the real system keeps a constant total energy. The numerical version above tends to add energy, so the oscillation can slowly grow even though the equation contains no driving force. A smaller dt delays the problem but does not change the method's behavior.
One small alternative is semi-implicit Euler. It updates velocity first, then uses the new velocity for position:
double acceleration = -(k / m) * x;
v = v + acceleration * dt;
x = x + v * dt;The order change looks minor, but it behaves much better for many mechanical systems. It is still an approximation, and it does not replace careful error analysis, but it is a useful reminder that implementation details can change the numerical physics.
Checking a Simulation
A program producing numbers is not evidence that the simulation is correct. I now look for a few basic checks:
- Does reducing
dtmake the result converge? - Does the output match a known solution in a simple case?
- Are quantities such as energy behaving as the model predicts?
- Do units remain consistent through every update?
- Does the result change unexpectedly with compiler or floating-point settings?
These checks catch mistakes that a convincing graph can hide.
What I Learned
- Differential equations become more approachable when treated as state updates.
- Euler's method is simple enough to understand, but not always accurate enough for serious simulations.
- The time step is a design choice, not just a number.
- Numerical stability and physical correctness are related, but not identical.
- More advanced methods like Runge-Kutta build on the same basic idea: approximate change over time.
This project made numerical methods feel less abstract. Instead of only seeing equations on paper, I could watch position and velocity evolve line by line.
— Maruf