Orbiting Planets in Solar System

Simulating Orbiting Planets in a Solar System Using Python (Orbiting Planets Series #1)

One of the many applications of programming in Python is simulating the real world. In some cases, the simulation is a way of solving a problem that would be difficult or impossible to solve using other means. In this article, you’ll explore simulating orbiting planets in a solar system using Python. You’ll create code that generates animations such as this one showing a binary star system:

This is the first article in the Orbiting Planets series in which you’ll simulate a solar system in two dimensions. You’ll also use the turtle module to deal with the graphical display.

In the second article in the series, you’ll move on to using Matplotlib to run and display the animation in both two and three dimensions.

The Tools for Simulating Orbiting Planets in Python

A solar system consists of one or more suns and other bodies orbiting the suns. In this simulation of a solar system, you’ll include suns and planets. However, you can extend the concept to other bodies such as moons, comets, and asteroids. The gravitational pull between the bodies determines the movement of all the bodies in the solar system.

At any point in time, a solar system body has a position and a velocity. In this project, you’ll be simulating a 2D solar system. Therefore, all the bodies in the solar system will exist in a 2D plane. The position of each body can be represented by a pair of values representing the x- and y-coordinates of the body. The velocity of a body is also represented by a pair of values which represent the components of the velocity along the x- and y-axes.

Any two bodies have a gravitational force that pulls them towards each other. This gravitational force F is given by:

F=G\frac{m_1m_2}{r^2}

G is the gravitational constant, which you’ll be able to ignore for this simulation since you’ll work in arbitrary units. The gravitational force depends on the mass of the two objects, m_1 and m_2, and the distance between the objects r. Although the masses are normally measured in kg and the distance in m, you’ll use arbitrary units for this simulation. This means that you’ll use values without any specific unit for the mass and distance. The numbers used for distance will represent the distance in pixels. This is the same reason why you can ignore the gravitational constant in this example.

The Python coding tools needed

Now that you’re familiar with the science you’ll need for the simulation, you can focus on the Python coding tools you’ll use for simulating orbiting planets. In this article, you’ll use the turtle module to deal with the graphics. This module provides a simple way of displaying graphics on the screen and moving items. It’s a basic graphics module, but it will allow you to focus on the main aspects of the simulation without worrying too much about the graphical part.

You don’t need to be familiar with the turtle module. I’ll explain the objects and methods you’ll need from this module in the article.

You’ll also use classes and object-oriented programming to create the solar system and the bodies within it. If you wish, you can read more about defining classes in Chapter 7: Object-Oriented Programming in The Python Coding Book.

Creating The Solar System and Its Bodies

You can create a module called solarsystem.py in which you can create the classes needed. You’ll need to define two main classes:

# solarsystem.py

import turtle


class SolarSystemBody(turtle.Turtle):
    ...


class SolarSystem:
    ...

The class SolarSystemBody can be used to create any of the bodies within a solar system. This includes suns and planets. This class inherits from the Turtle class in the turtle module. Therefore, when you create an instance of the class SolarSystemBody, this instance will also have access to all the attributes of the Turtle class.

The SolarSystem class is used to create the entire solar system, which contains several bodies. This class will also control how bodies interact with each other.

The ellipsis ... are used for the time being as placeholders. You’ll replace these with actual code soon. Including ellipsis ensures that the program doesn’t raise an error if you run it.

Before starting to write the code, you can define two more subclasses:

# solarsystem.py

import turtle


class SolarSystemBody(turtle.Turtle):
    ...


class Sun(SolarSystemBody):
    ...


class Planet(SolarSystemBody):
    ...


class SolarSystem:
    ...

The classes Sun and Planet inherit from SolarSystemBody, and they’re a convenient way of treating suns and planets slightly differently.

Setting up the solar system

The SolarSystem class keeps track of all the bodies it contains, and it also deals with displaying the window in which all bodies will be drawn. You can create the __init__() method for this class and a couple of additional methods:

# solarsystem.py

import turtle


# Solar System Bodies
class SolarSystemBody(turtle.Turtle):
    ...


class Sun(SolarSystemBody):
    ...


class Planet(SolarSystemBody):
    ...


# Solar System
class SolarSystem:
    def __init__(self, width, height):
        self.solar_system = turtle.Screen()
        self.solar_system.tracer(0)
        self.solar_system.setup(width, height)
        self.solar_system.bgcolor("black")

        self.bodies = []

    def add_body(self, body):
        self.bodies.append(body)

    def remove_body(self, body):
        self.bodies.remove(body)

The __init__() method has two parameters that define the width and height of the window containing the solar system. The method creates a solar_system attribute which is an instance of the object returned by turtle.Screen(). You then use three methods from the turtle module to set up the window:

  • tracer(0) gives you more control over when items are drawn in the window. I won’t go into detail on why we need this method, but you can read more about it in the turtle module documentation
  • setup() sets the width and height of the window in pixels
  • bgcolor() changes the background colour of the window

You create another attribute for the class SolarSystem called bodies. This attribute stores a list that can hold all the bodies present in the solar system.

You also define two methods, add_body() and remove_body(), which add and remove bodies from the bodies attribute.

Creating Solar System Bodies

The __init__() method for the SolarSystemBodies class needs to define the mass, position, and velocity of the body. It also needs to link the body to a solar system. These requirements are reflected in the parameters of the __init__() method:

# solarsystem.py

import turtle


# Solar System Bodies
class SolarSystemBody(turtle.Turtle):
    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__()
        self.mass = mass
        self.setposition(position)
        self.velocity = velocity

        self.penup()
        self.hideturtle()

        solar_system.add_body(self)


class Sun(SolarSystemBody):
    ...


class Planet(SolarSystemBody):
    ...


# Solar System
class SolarSystem:
    def __init__(self, width, height):
        self.solar_system = turtle.Screen()
        self.solar_system.tracer(0)
        self.solar_system.setup(width, height)
        self.solar_system.bgcolor("black")

        self.bodies = []

    def add_body(self, body):
        self.bodies.append(body)

    def remove_body(self, body):
        self.bodies.remove(body)

The position and velocity attributes are both tuples, each containing two values. The position attribute contains the x- and y-coordinates. The velocity attribute has the velocity components along the two axes. The default value for both is the tuple (0, 0), which means that an instance of class SolarSystemBody defaults to being stationary in the middle of the window when you first create it.

setposition(), penup() and hideturtle() are methods from the turtle module. You’re using setposition() to place the body at a particular set of coordinates on the screen. penup() ensures the body doesn’t draw any lines as it moves and hideturtle() hides the object that does the drawing.

You’re also calling the add_body() method of the SolarSystem class, which you’ve defined earlier. Therefore, whenever you create a SolarSystemBody, you’re always making sure it’s linked to the solar system it belongs to.

Displaying the bodies

Now, you can create another method to draw the sun or planet. To keep things straightforward, you can determine the display size of each body directly from its mass. However, you’ll need to make a couple of adjustments. Suns are much heavier than planets, so it’s best if you use a logarithmic scale to convert from mass to display size. You also want to set a minimum display size. Otherwise, bodies that are not very heavy will not be visible. You can achieve both of these by creating and defining a display_size attribute and two class attributes called min_display_size and display_log_base:

# solarsystem.py

import math
import turtle


# Solar System Bodies
class SolarSystemBody(turtle.Turtle):
    min_display_size = 20
    display_log_base = 1.1

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__()
        self.mass = mass
        self.setposition(position)
        self.velocity = velocity
        self.display_size = max(
            math.log(self.mass, self.display_log_base),
            self.min_display_size,
        )

        self.penup()
        self.hideturtle()

        solar_system.add_body(self)


class Sun(SolarSystemBody):
    ...


class Planet(SolarSystemBody):
    ...


# Solar System
class SolarSystem:
    def __init__(self, width, height):
        self.solar_system = turtle.Screen()
        self.solar_system.tracer(0)
        self.solar_system.setup(width, height)
        self.solar_system.bgcolor("black")

        self.bodies = []

    def add_body(self, body):
        self.bodies.append(body)

    def remove_body(self, body):
        self.bodies.remove(body)

display_log_base defines the base for the logarithm used to convert from mass to display size. You use this class attribute as the second argument in the math.log() function. The max() function ensures that if the calculated display size is smaller than min_display_size, this minimum value is used instead.

You’re almost ready to try out the classes you’ve written so far. There’s one more method you’ll need to define before testing the code:

# solarsystem.py

import math
import turtle


# Solar System Bodies
class SolarSystemBody(turtle.Turtle):
    min_display_size = 20
    display_log_base = 1.1

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__()
        self.mass = mass
        self.setposition(position)
        self.velocity = velocity
        self.display_size = max(
            math.log(self.mass, self.display_log_base),
            self.min_display_size,
        )

        self.penup()
        self.hideturtle()

        solar_system.add_body(self)

    def draw(self):
        self.dot(self.display_size)


class Sun(SolarSystemBody):
    ...


class Planet(SolarSystemBody):
    ...


# Solar System
class SolarSystem:
    def __init__(self, width, height):
        self.solar_system = turtle.Screen()
        self.solar_system.tracer(0)
        self.solar_system.setup(width, height)
        self.solar_system.bgcolor("black")

        self.bodies = []

    def add_body(self, body):
        self.bodies.append(body)

    def remove_body(self, body):
        self.bodies.remove(body)

The draw() method uses dot() from the turtle module to draw a dot of the required size.

Creating a Sun

To test your code so far, you can create and display a sun. To do this, you can add an __init__() method to the Sun subclass:

# solarsystem.py

import math
import turtle


# Solar System Bodies
class SolarSystemBody(turtle.Turtle):
    min_display_size = 20
    display_log_base = 1.1

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__()
        self.mass = mass
        self.setposition(position)
        self.velocity = velocity
        self.display_size = max(
            math.log(self.mass, self.display_log_base),
            self.min_display_size,
        )

        self.penup()
        self.hideturtle()

        solar_system.add_body(self)

    def draw(self):
        self.dot(self.display_size)


class Sun(SolarSystemBody):
    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color("yellow")



class Planet(SolarSystemBody):
    ...


# Solar System
class SolarSystem:
    def __init__(self, width, height):
        self.solar_system = turtle.Screen()
        self.solar_system.tracer(0)
        self.solar_system.setup(width, height)
        self.solar_system.bgcolor("black")

        self.bodies = []

    def add_body(self, body):
        self.bodies.append(body)

    def remove_body(self, body):
        self.bodies.remove(body)

You’re using the color() method from the turtle module to change the colour of a sun to yellow.

To test your code so far, you can create a second script called simple_solar_system.py in which you can create and display a sun:

# simple_solar_system.py

from solarsystem import SolarSystem, Sun

solar_system = SolarSystem(width=1400, height=900)

sun = Sun(solar_system, mass=10_000)

sun.draw()

# Temporary lines
import turtle
turtle.done()

You’re importing the classes SolarSystem and Sun from the solarsystem module, and you’re creating instances of both classes. When you create sun, you’re using the default values for position and velocity. Finally, you use the draw() method of the Sun class.

To keep the window open at the end of the program, you add two temporary lines, which you won’t need later on. From the turtle module, you use the function done(), which keeps the display window open. The code above displays a yellow sun in the middle of the screen:

Making The Solar System Bodies Move

It’s time to add the move() method to SolarSystemBody. Any movement is made up of a component along the x-axis and another along the y-axis. There are two pairs of turtle methods that will be useful:

  • setx() and sety() change the x– and y-coordinates of the Turtle object
  • xcor() and ycor() return the current x– and y-coordinates of the Turtle object

You can combine these into the move() method, and you can add an extra line to the draw() method, which clears the previous drawing before redrawing the body. The clear() method is part of the turtle module:

# solarsystem.py

import math
import turtle


# Solar System Bodies
class SolarSystemBody(turtle.Turtle):
    min_display_size = 20
    display_log_base = 1.1

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__()
        self.mass = mass
        self.setposition(position)
        self.velocity = velocity
        self.display_size = max(
            math.log(self.mass, self.display_log_base),
            self.min_display_size,
        )

        self.penup()
        self.hideturtle()

        solar_system.add_body(self)

    def draw(self):
        self.clear()
        self.dot(self.display_size)

    def move(self):
        self.setx(self.xcor() + self.velocity[0])
        self.sety(self.ycor() + self.velocity[1])


class Sun(SolarSystemBody):
    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color("yellow")



class Planet(SolarSystemBody):
    ...


# Solar System
class SolarSystem:
    def __init__(self, width, height):
        self.solar_system = turtle.Screen()
        self.solar_system.tracer(0)
        self.solar_system.setup(width, height)
        self.solar_system.bgcolor("black")

        self.bodies = []

    def add_body(self, body):
        self.bodies.append(body)

    def remove_body(self, body):
        self.bodies.remove(body)

The draw() and move() methods you defined allow you to control each body in the solar system. However, you’ll always want to deal with all the bodies in the solar system at the same time. Therefore, you can let the SolarSystem class manage the movement of all the bodies within it. You can create a new method of the SolarSystem class:

# solarsystem.py

import math
import turtle


# Solar System Bodies
class SolarSystemBody(turtle.Turtle):
    min_display_size = 20
    display_log_base = 1.1

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__()
        self.mass = mass
        self.setposition(position)
        self.velocity = velocity
        self.display_size = max(
            math.log(self.mass, self.display_log_base),
            self.min_display_size,
        )

        self.penup()
        self.hideturtle()

        solar_system.add_body(self)

    def draw(self):
        self.clear()
        self.dot(self.display_size)

    def move(self):
        self.setx(self.xcor() + self.velocity[0])
        self.sety(self.ycor() + self.velocity[1])


class Sun(SolarSystemBody):
    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color("yellow")



class Planet(SolarSystemBody):
    ...


# Solar System
class SolarSystem:
    def __init__(self, width, height):
        self.solar_system = turtle.Screen()
        self.solar_system.tracer(0)
        self.solar_system.setup(width, height)
        self.solar_system.bgcolor("black")

        self.bodies = []

    def add_body(self, body):
        self.bodies.append(body)

    def remove_body(self, body):
        self.bodies.remove(body)

    def update_all(self):
        for body in self.bodies:
            body.move()
            body.draw()
        self.solar_system.update()

The update_all() method goes through all the solar system bodies stored in the bodies attribute. It moves and draws them all. Finally, it calls the update() method from turtle, which redraws all items on the screen.

You can now use this new SolarSystem method in simple_solar_system.py:

# simple_solar_system.py

from solarsystem import SolarSystem, Sun

solar_system = SolarSystem(width=1400, height=900)

sun = Sun(solar_system, mass=10_000, velocity=(2, 1))

while True:
    solar_system.update_all()

You’ve included the velocity argument when you created the instance of Sun. The repeated calls to solar_system.update_all() create the following animation showing the sun moving away from the centre of the solar system:

You can now create a solar system body and make it move with any velocity you wish. However, the fun starts when you also add a planet into the mix.

Creating A Planet

It’s time to finish the Planet class now. You’ll create planets that alternate between red, green, and blue in this simulation using itertools.cycle():

# solarsystem.py

import itertools
import math
import turtle


# Solar System Bodies
class SolarSystemBody(turtle.Turtle):
    min_display_size = 20
    display_log_base = 1.1

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__()
        self.mass = mass
        self.setposition(position)
        self.velocity = velocity
        self.display_size = max(
            math.log(self.mass, self.display_log_base),
            self.min_display_size,
        )

        self.penup()
        self.hideturtle()

        solar_system.add_body(self)

    def draw(self):
        self.clear()
        self.dot(self.display_size)

    def move(self):
        self.setx(self.xcor() + self.velocity[0])
        self.sety(self.ycor() + self.velocity[1])


class Sun(SolarSystemBody):
    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color("yellow")



class Planet(SolarSystemBody):
    colours = itertools.cycle(["red", "green", "blue"])

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color(next(Planet.colours))


# Solar System
class SolarSystem:
    def __init__(self, width, height):
        self.solar_system = turtle.Screen()
        self.solar_system.tracer(0)
        self.solar_system.setup(width, height)
        self.solar_system.bgcolor("black")

        self.bodies = []

    def add_body(self, body):
        self.bodies.append(body)

    def remove_body(self, body):
        self.bodies.remove(body)

    def update_all(self):
        for body in self.bodies:
            body.move()
            body.draw()
        self.solar_system.update()

You can now go back to simple_solar_system.py and create a stationary sun in the centre and a planet off-centre:

# simple_solar_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

sun = Sun(solar_system, mass=10_000)
planet = Planet(
    solar_system,
    mass=1,
    position=(-350, 0),
    velocity=(0, 5),
)

while True:
    solar_system.update_all()

You create the planet on the left-hand side of the window. Its velocity along the x-axis is 0, and the velocity along the y-axis is 5. You’ll recall these are arbitrary units, so the velocity is 5 pixels per frame. This code gives the following output:

However, the animation so far doesn’t take into account the gravitational pull between the sun and the planet.

Gravity

At the start of this article, I summarised the physics of the gravitational force between two objects. Since you’re using arbitrary units in this example, you can simplify the force between two bodies as:

F = \frac{m_1m_2}{r^2}

The effect of a force is to accelerate the object. The relationship between the force exerted on a body, the acceleration, and the body’s mass is given by:

F = ma

The term a represents the acceleration. If you have the force and mass, you can work out the acceleration using:

a=\frac{F}{m}

Therefore, you can work out the gravitational force between two objects and then calculate the acceleration this force causes for each body.

The force has a direction, too. It acts in the direction of the line joining the centres of the two bodies. The acceleration of the two bodies also acts along this same direction. However, you’re dealing with the x– and y-components of the velocity. Therefore, you’ll need to find the x– and y-components of the acceleration, too. You can achieve this through trigonometry:

a_x = a\cos(\theta)
a_y = a\sin(\theta)

The \cos and \sin trigonometric functions can be used to give the x– and y-components of the acceleration. \theta represents the angle that the line joining the two bodies makes with the horizontal.

Accounting for gravity in the simulation

You can include the steps outlined above into a method that works out the change in velocity of both bodies along both directions, x and y. This method fits best as part of the SolarSystem class but can be a static method:

# solarsystem.py

import itertools
import math
import turtle


# Solar System Bodies
class SolarSystemBody(turtle.Turtle):
    min_display_size = 20
    display_log_base = 1.1

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__()
        self.mass = mass
        self.setposition(position)
        self.velocity = velocity
        self.display_size = max(
            math.log(self.mass, self.display_log_base),
            self.min_display_size,
        )

        self.penup()
        self.hideturtle()

        solar_system.add_body(self)

    def draw(self):
        self.clear()
        self.dot(self.display_size)

    def move(self):
        self.setx(self.xcor() + self.velocity[0])
        self.sety(self.ycor() + self.velocity[1])


class Sun(SolarSystemBody):
    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color("yellow")



class Planet(SolarSystemBody):
    colours = itertools.cycle(["red", "green", "blue"])

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color(next(Planet.colours))


# Solar System
class SolarSystem:
    def __init__(self, width, height):
        self.solar_system = turtle.Screen()
        self.solar_system.tracer(0)
        self.solar_system.setup(width, height)
        self.solar_system.bgcolor("black")

        self.bodies = []

    def add_body(self, body):
        self.bodies.append(body)

    def remove_body(self, body):
        self.bodies.remove(body)

    def update_all(self):
        for body in self.bodies:
            body.move()
            body.draw()
        self.solar_system.update()

    @staticmethod
    def accelerate_due_to_gravity(
            first: SolarSystemBody,
            second: SolarSystemBody,
    ):
        force = first.mass * second.mass / first.distance(second) ** 2
        angle = first.towards(second)
        reverse = 1
        for body in first, second:
            acceleration = force / body.mass
            acc_x = acceleration * math.cos(math.radians(angle))
            acc_y = acceleration * math.sin(math.radians(angle))
            body.velocity = (
                body.velocity[0] + (reverse * acc_x),
                body.velocity[1] + (reverse * acc_y),
            )
            reverse = -1

The static method accelerate_due_to_gravity() accepts two arguments of type SolarSystemBody. The method signature uses type hinting for clarity.

You then use the force calculated to work out the acceleration of each body and break down this acceleration into acc_x and acc_y, the x– and y-components. Note that the angle returned by the towards() method in turtle is in degrees. You’ll need to convert it to radians before using it as an argument for math.sin() and math.cos().

The velocity is measured in pixels/frame in this simulation as you’re using arbitrary units. The acceleration is therefore measured in pixels/frame2. Therefore, in each frame of the animation you can add the x– and y-acceleration components to the velocity components to get the body’s new velocity. The acceleration changes sign between the two bodies as the bodies accelerate towards each other. The reverse variable achieves this.

You can try out this method in simple_solar_system.py:

# simple_solar_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

sun = Sun(solar_system, mass=10_000)
planet = Planet(
    solar_system,
    mass=1,
    position=(-350, 0),
    velocity=(0, 5),
)

while True:
    solar_system.accelerate_due_to_gravity(sun, planet)
    solar_system.update_all()

This code now gives the following animation:

The acceleration caused by the gravitational pull causes the planet to change direction as it moves. In this case, the planet orbits the sun. The sun’s velocity is also changing. However, as the sun’s mass is much larger than the planet’s mass, the same force only causes a negligible change in the sun’s velocity.

Depending on the planet’s initial position and velocity, you could end up with the planet crashing into the sun or escaping the solar system.

Let’s look at the case when the planet crashes into the sun. You can achieve this by setting the planet to a lower initial velocity:

# simple_solar_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

sun = Sun(solar_system, mass=10_000)
planet = Planet(
    solar_system,
    mass=1,
    position=(-350, 0),
    velocity=(0, 1),
)

while True:
    solar_system.accelerate_due_to_gravity(sun, planet)
    solar_system.update_all()

As the planet doesn’t have sufficient initial velocity, it’s pulled in towards the sun. Here’s the output of this code:

The code currently only relies on the distance between the centres of the bodies. You’ll need to detect and account for the case when the planet crashes into the sun. You can create another method to check for collisions and remove the planet if there’s a collision between the planet and the sun:

# solarsystem.py

import itertools
import math
import turtle


# Solar System Bodies
class SolarSystemBody(turtle.Turtle):
    min_display_size = 20
    display_log_base = 1.1

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__()
        self.mass = mass
        self.setposition(position)
        self.velocity = velocity
        self.display_size = max(
            math.log(self.mass, self.display_log_base),
            self.min_display_size,
        )

        self.penup()
        self.hideturtle()

        solar_system.add_body(self)

    def draw(self):
        self.clear()
        self.dot(self.display_size)

    def move(self):
        self.setx(self.xcor() + self.velocity[0])
        self.sety(self.ycor() + self.velocity[1])


class Sun(SolarSystemBody):
    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color("yellow")



class Planet(SolarSystemBody):
    colours = itertools.cycle(["red", "green", "blue"])

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color(next(Planet.colours))


# Solar System
class SolarSystem:
    def __init__(self, width, height):
        self.solar_system = turtle.Screen()
        self.solar_system.tracer(0)
        self.solar_system.setup(width, height)
        self.solar_system.bgcolor("black")

        self.bodies = []

    def add_body(self, body):
        self.bodies.append(body)

    def remove_body(self, body):
        self.bodies.remove(body)

    def update_all(self):
        for body in self.bodies:
            body.move()
            body.draw()
        self.solar_system.update()

    @staticmethod
    def accelerate_due_to_gravity(
            first: SolarSystemBody,
            second: SolarSystemBody,
    ):
        force = first.mass * second.mass / first.distance(second) ** 2
        angle = first.towards(second)
        reverse = 1
        for body in first, second:
            acceleration = force / body.mass
            acc_x = acceleration * math.cos(math.radians(angle))
            acc_y = acceleration * math.sin(math.radians(angle))
            body.velocity = (
                body.velocity[0] + (reverse * acc_x),
                body.velocity[1] + (reverse * acc_y),
            )
            reverse = -1

    def check_collision(self, first, second):
        if first.distance(second) < first.display_size/2 + second.display_size/2:
            for body in first, second:
                if isinstance(body, Planet):
                    self.remove_body(body)

You’re detecting the collision by comparing the distance between the two bodies with the sum of the radii of the two bodies. However, you only want to remove the planet and not the sun. This is where having two different subclasses for Sun and Planet comes in useful as you can use the isinstance() built-in function to check what type of body you’re dealing with at any time. You’ll test this method shortly, but first, you’ll need to deal with more than two solar system bodies.

Adding More Solar System Bodies

You can add a second planet to simple_solar_system.py

# simple_solar_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

sun = Sun(solar_system, mass=10_000)
planets = (
    Planet(
        solar_system,
        mass=1,
        position=(-350, 0),
        velocity=(0, 5),
    ),
    Planet(
        solar_system,
        mass=2,
        position=(-270, 0),
        velocity=(0, 7),
    ),
)

while True:
    solar_system.update_all()

In addition to adding a second planet, you also removed the call to accelerate_due_to_gravity() in the while loop. Since you have three bodies in the solar system, you now need to take care of all possible interactions. These include the interactions between:

  • the first planet and the sun
  • the second planet and the sun
  • the two planets

The more bodies you have in your solar system, the more interactions you’ll need to account for. You can write another method in the SolarSystem class to manage all these interactions.

You can loop through the list stored in the solar system’s bodies attribute. For each body in this list, you can account for the interaction between this body and all the bodies that come after it in the list. By only considering interactions with bodies that come later on in the list, you’re ensuring you don’t account for the same interactions twice:

# solarsystem.py

import itertools
import math
import turtle


# Solar System Bodies
class SolarSystemBody(turtle.Turtle):
    min_display_size = 20
    display_log_base = 1.1

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__()
        self.mass = mass
        self.setposition(position)
        self.velocity = velocity
        self.display_size = max(
            math.log(self.mass, self.display_log_base),
            self.min_display_size,
        )

        self.penup()
        self.hideturtle()

        solar_system.add_body(self)

    def draw(self):
        self.clear()
        self.dot(self.display_size)

    def move(self):
        self.setx(self.xcor() + self.velocity[0])
        self.sety(self.ycor() + self.velocity[1])


class Sun(SolarSystemBody):
    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color("yellow")



class Planet(SolarSystemBody):
    colours = itertools.cycle(["red", "green", "blue"])

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color(next(Planet.colours))


# Solar System
class SolarSystem:
    def __init__(self, width, height):
        self.solar_system = turtle.Screen()
        self.solar_system.tracer(0)
        self.solar_system.setup(width, height)
        self.solar_system.bgcolor("black")

        self.bodies = []

    def add_body(self, body):
        self.bodies.append(body)

    def remove_body(self, body):
        self.bodies.remove(body)

    def update_all(self):
        for body in self.bodies:
            body.move()
            body.draw()
        self.solar_system.update()

    @staticmethod
    def accelerate_due_to_gravity(
            first: SolarSystemBody,
            second: SolarSystemBody,
    ):
        force = first.mass * second.mass / first.distance(second) ** 2
        angle = first.towards(second)
        reverse = 1
        for body in first, second:
            acceleration = force / body.mass
            acc_x = acceleration * math.cos(math.radians(angle))
            acc_y = acceleration * math.sin(math.radians(angle))
            body.velocity = (
                body.velocity[0] + (reverse * acc_x),
                body.velocity[1] + (reverse * acc_y),
            )
            reverse = -1

    def check_collision(self, first, second):
        if first.distance(second) < first.display_size/2 + second.display_size/2:
            for body in first, second:
                if isinstance(body, Planet):
                    self.remove_body(body)

    def calculate_all_body_interactions(self):
        bodies_copy = self.bodies.copy()
        for idx, first in enumerate(bodies_copy):
            for second in bodies_copy[idx + 1:]:
                self.accelerate_due_to_gravity(first, second)
                self.check_collision(first, second)

You’re creating a copy of self.bodies since the method check_collision() can remove items from the list, and therefore, you shouldn’t iterate through a list that can change while the loop is running. In the inner loop, you’re iterating through the part of the list that comes after the current item using the slice [idx + 1:].

You can now test your simulation so far with one sun and two planets. First, you can test the following scenario:

# simple_solar_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

sun = Sun(solar_system, mass=10_000)
planets = (
    Planet(
        solar_system,
        mass=1,
        position=(-350, 0),
        velocity=(0, 5),
    ),
    Planet(
        solar_system,
        mass=2,
        position=(-270, 0),
        velocity=(0, 7),
    ),
)

while True:
    solar_system.calculate_all_body_interactions()
    solar_system.update_all()

The two planets orbit the sun, as shown in the following video:

You can also try changing the initial velocity of the first planet to (0, 1). You’ll see that the planet crashes into the sun, and the planet is removed from the simulation. In the current version, you’ll see the planet gets “stuck” in its last position. However, you can add body.clear() to the remove_body() method in the SolarSystem class. This clears the drawing of the body when it’s removed from the solar system.

You can also add an extra condition to check_collision() to ignore collisions between two planets. As this is a 2D simulation, you can justify this change. Otherwise, as you add more planets, you’re more likely they will overlap at some point during the simulation, and therefore, crash into each other:

# solarsystem.py

import itertools
import math
import turtle


# Solar System Bodies
class SolarSystemBody(turtle.Turtle):
    min_display_size = 20
    display_log_base = 1.1

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__()
        self.mass = mass
        self.setposition(position)
        self.velocity = velocity
        self.display_size = max(
            math.log(self.mass, self.display_log_base),
            self.min_display_size,
        )

        self.penup()
        self.hideturtle()

        solar_system.add_body(self)

    def draw(self):
        self.clear()
        self.dot(self.display_size)

    def move(self):
        self.setx(self.xcor() + self.velocity[0])
        self.sety(self.ycor() + self.velocity[1])


class Sun(SolarSystemBody):
    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color("yellow")



class Planet(SolarSystemBody):
    colours = itertools.cycle(["red", "green", "blue"])

    def __init__(
            self,
            solar_system,
            mass,
            position=(0, 0),
            velocity=(0, 0),
    ):
        super().__init__(solar_system, mass, position, velocity)
        self.color(next(Planet.colours))


# Solar System
class SolarSystem:
    def __init__(self, width, height):
        self.solar_system = turtle.Screen()
        self.solar_system.tracer(0)
        self.solar_system.setup(width, height)
        self.solar_system.bgcolor("black")

        self.bodies = []

    def add_body(self, body):
        self.bodies.append(body)

    def remove_body(self, body):
        body.clear()
        self.bodies.remove(body)

    def update_all(self):
        for body in self.bodies:
            body.move()
            body.draw()
        self.solar_system.update()

    @staticmethod
    def accelerate_due_to_gravity(
            first: SolarSystemBody,
            second: SolarSystemBody,
    ):
        force = first.mass * second.mass / first.distance(second) ** 2
        angle = first.towards(second)
        reverse = 1
        for body in first, second:
            acceleration = force / body.mass
            acc_x = acceleration * math.cos(math.radians(angle))
            acc_y = acceleration * math.sin(math.radians(angle))
            body.velocity = (
                body.velocity[0] + (reverse * acc_x),
                body.velocity[1] + (reverse * acc_y),
            )
            reverse = -1

    def check_collision(self, first, second):
        if isinstance(first, Planet) and isinstance(second, Planet):
            return
        if first.distance(second) < first.display_size/2 + second.display_size/2:
            for body in first, second:
                if isinstance(body, Planet):
                    self.remove_body(body)

    def calculate_all_body_interactions(self):
        bodies_copy = self.bodies.copy()
        for idx, first in enumerate(bodies_copy):
            for second in bodies_copy[idx + 1:]:
                self.accelerate_due_to_gravity(first, second)
                self.check_collision(first, second)

This completes the solarsystem module. You can now use this module to create other solar systems.

Creating A Binary Star System

Let’s finish off with another example of simulating orbiting planets in a solar system using Python. You’ll simulate a binary star system. These are solar systems with two stars that orbit around their centre of mass. You’ve already got all the tools you need to create this or any solar system you wish. These tools are the classes you defined in the solarsystem module.

You can create a new script called binary_star_system.py, import the classes from solarsystem and create an instance of the SolarSystem class:

# binary_star_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

To create a binary star system, you can:

  • create two stars and set their initial positions and velocities so that the stars orbit each other
  • launch several planets and find ones that create stable orbits

Let’s start by creating the two stars:

# binary_star_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

suns = (
    Sun(solar_system, mass=10_000, position=(-200, 0)),
    Sun(solar_system, mass=10_000, position=(200, 0)),
)

while True:
    solar_system.calculate_all_body_interactions()
    solar_system.update_all()

In this example, you created the suns displaced from each other along the horizontal. However, you set them with an initial velocity of (0, 0) as you’re using the default value for the velocity parameter when creating the instances of Sun.

This leads to the following result:

The suns don’t stay stationary for too long as the gravitational force pulls them towards each other. In this simulation, they accelerate towards each other and then cross each other and fly out of the solar system! In reality, the suns will crash into each other. You can modify your code to account for this if you wish. However, I won’t make this change in this article as you’ll focus on stable binary star systems.

To make the stars orbit each other, you’ll need to give them an initial velocity. Here’s a pair of velocities that gives a stable binary star system:

# binary_star_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

suns = (
    Sun(solar_system, mass=10_000, position=(-200, 0), velocity=(0, 3.5)),
    Sun(solar_system, mass=10_000, position=(200, 0), velocity=(0, -3.5)),
)

while True:
    solar_system.calculate_all_body_interactions()
    solar_system.update_all()

This code gives the following animation of the binary star system:

Now, you can create some planets and launch them from a particular position in the solar system and with an initial velocity.

Adding planets to the binary star solar system

You can start by adding one planet and experiment with its initial position and velocity. In the example below, you place the planet at the centre of the solar system by using the default position of (0, 0) and give it an initial velocity of (2, 2):

# binary_star_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

suns = (
    Sun(solar_system, mass=10_000, position=(-200, 0), velocity=(0, 3.5)),
    Sun(solar_system, mass=10_000, position=(200, 0), velocity=(0, -3.5)),
)

planet = Planet(solar_system, mass=20, velocity=(2, 2))

while True:
    solar_system.calculate_all_body_interactions()
    solar_system.update_all()

This velocity means that the planet launches at an angle of 45º, but it comes under the strong effect of the closest sun very quickly:

You can increase the planet’s initial velocity to (3, 3):

# binary_star_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

suns = (
    Sun(solar_system, mass=10_000, position=(-200, 0), velocity=(0, 3.5)),
    Sun(solar_system, mass=10_000, position=(200, 0), velocity=(0, -3.5)),
)

planet = Planet(solar_system, mass=20, velocity=(3, 3))

while True:
    solar_system.calculate_all_body_interactions()
    solar_system.update_all()

As you can see from the resulting animation below, the planet starts off orbiting one of the suns, but this is not a stable orbit, and it doesn’t take long for the planet to crash and burn into the sun:

You can now try launching the planet with a different initial velocity. In the example below, you’ll launch the planet vertically with a higher initial velocity of (0, 11):

# binary_star_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

suns = (
    Sun(solar_system, mass=10_000, position=(-200, 0), velocity=(0, 3.5)),
    Sun(solar_system, mass=10_000, position=(200, 0), velocity=(0, -3.5)),
)

planet = Planet(solar_system, mass=20, velocity=(0, 11))

while True:
    solar_system.calculate_all_body_interactions()
    solar_system.update_all()

The planet survives for longer in this case. Its orbit is affected by the gravitational pull from both suns. At times it’s closer to one of the suns and is affected by its gravitational pull more. At other times, it’s roughly equidistant from the suns, and both stars will have a similar gravitational pull on the planet:

A small change in initial conditions can make a large difference in the final outcome. In the following example, you shift the starting position of the planet 50 pixels to the right by setting the initial position to (50, 0):

# binary_star_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

suns = (
    Sun(solar_system, mass=10_000, position=(-200, 0), velocity=(0, 3.5)),
    Sun(solar_system, mass=10_000, position=(200, 0), velocity=(0, -3.5)),
)

planet = Planet(solar_system, mass=20, position=(50, 0), velocity=(0, 11))

while True:
    solar_system.calculate_all_body_interactions()
    solar_system.update_all()

This gives an orbit that’s more stable and significantly different from the previous case:

You can now add a second planet. You create this planet to the left of the solar system, and it’s initially moving vertically downwards:

# binary_star_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

suns = (
    Sun(solar_system, mass=10_000, position=(-200, 0), velocity=(0, 3.5)),
    Sun(solar_system, mass=10_000, position=(200, 0), velocity=(0, -3.5)),
)

planets = (
    Planet(solar_system, mass=20, position=(50, 0), velocity=(0, 11)),
    Planet(solar_system, mass=3, position=(-350, 0), velocity=(0, -10)),
)

while True:
    solar_system.calculate_all_body_interactions()
    solar_system.update_all()

You’ll recall that you’ve set the colours of the planets to cycle through red, green, and blue. The second planet you add will therefore have a green colour in the animation:

In this case, you’ll see the green planet orbiting both suns in a relatively smooth orbit, whereas the red planet has a more chaotic path zig-zagging in between the suns.

You can finish off with one final planet:

# binary_star_system.py

from solarsystem import SolarSystem, Sun, Planet

solar_system = SolarSystem(width=1400, height=900)

suns = (
    Sun(solar_system, mass=10_000, position=(-200, 0), velocity=(0, 3.5)),
    Sun(solar_system, mass=10_000, position=(200, 0), velocity=(0, -3.5)),
)

planets = (
    Planet(solar_system, mass=20, position=(50, 0), velocity=(0, 11)),
    Planet(solar_system, mass=3, position=(-350, 0), velocity=(0, -10)),
    Planet(solar_system, mass=1, position=(0, 200), velocity=(-2, -7)),
)

while True:
    solar_system.calculate_all_body_interactions()
    solar_system.update_all()

The third planet you’ve added is the blue one in this animation. It doesn’t survive for very long:

You may have noticed that while working on the binary star solar system in this section, you didn’t have to modify the solarsystem.py module in any way. Once you’ve defined the classes, you can use them in several different simulations.

It’s now up to you to experiment with more solar systems!

The final versions of the code used in this article are also available on this GitHub repo.

Final Words

In this article, you’ve learned about simulating orbiting planets in a solar system using Python. As with all simulations of real-world situations, you’ve had to make some simplifications. In this case, the main simplification you’ve made is to reduce the solar system into a two-dimensional plane. You’ve also used the turtle module to deal with the graphics for this simulation.

In a second article in the Orbiting Planets Series, you’ll look at how to extend this simulation to three dimensions using Matplotlib.

Even though this simulation of orbiting planets and solar systems relies on a number of simplifications, it gives a good insight into how you can use Python programming to represent real-world situations. In this simulation, you looked at an example from the physical world using knowledge about the motion of stars and planets and the gravitational attraction between them.

Further Reading


Get the latest blog updates

No spam promise. You’ll get an email when a new blog post is published


Leave a Reply