├── .gitignore ├── Color.py ├── Main.py ├── Particle.py ├── README.md ├── Simulations.py └── Ticker.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /Color.py: -------------------------------------------------------------------------------- 1 | from random import sample 2 | 3 | class Color: 4 | 5 | WHITE = (255,255,255) 6 | DGREY = (70,70,70) 7 | MGREY = (190,190,190) 8 | LGREY = (230,230,230) 9 | NAVY = (20,20,80) 10 | BLUE = (40,40,200) 11 | GREEN = (40,200,40) 12 | RED = (200,40,40) 13 | GOLD = (240, 190, 50) 14 | 15 | def random_vibrant(): 16 | """ 17 | Returns a random vibrant color in RGB format. Works by forcing a 18 | large enough difference between the R, G, and B values. 19 | """ 20 | while True: 21 | R, G, B = sample(range(0,256), 3) 22 | diff_sum = abs(R-G) + abs(G-B) + abs(R-B) 23 | if diff_sum > 200: 24 | return (R, G, B) 25 | 26 | def random_dull(): 27 | """ 28 | Returns a random dull color in RGB format. Works by forcing the RGB 29 | difference to be between two values. 30 | """ 31 | while True: 32 | R, G, B = sample(range(0,256), 3) 33 | diff_sum = abs(R-G) + abs(G-B) + abs(R-B) 34 | totl_sum = R + G + B 35 | if diff_sum > 80 and diff_sum < 200 and totl_sum > 200 and totl_sum < 500: 36 | return (R, G, B) 37 | 38 | 39 | 40 | class ColorGradient: 41 | """ 42 | A color gradient object contains two colors, and the number of partitions 43 | for the desired color gradient. The get_color function returns a color 44 | that is 'between' the color1 and color2 at the given partition. 45 | """ 46 | 47 | def __init__(self, color_1, color_2, nr_partitions): 48 | self.c1 = color_1 49 | self.c2 = color_2 50 | self.parts = nr_partitions 51 | 52 | def get_color(self, part): 53 | """ 54 | Get RGB color values of the color that sits between color1 and color2 55 | at the desired partition part should be >= 0 and <= nr_partitions. 56 | """ 57 | R_width = self.c2[0] - self.c1[0] 58 | G_width = self.c2[1] - self.c1[1] 59 | B_width = self.c2[2] - self.c1[2] 60 | R = int(self.c1[0] + (part / self.parts) * R_width) 61 | G = int(self.c1[1] + (part / self.parts) * G_width) 62 | B = int(self.c1[2] + (part / self.parts) * B_width) 63 | return (R, G, B) 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import time 3 | from Color import Color 4 | from Ticker import Ticker 5 | from Simulations import Random_sim, Solar_system, User_controlled 6 | 7 | 8 | """ 9 | TODO: 10 | - Zooming function 11 | - Drawer class that can draw and update figures based on a list of points and a rotation 12 | - Player controlled node with engine and particles 13 | - Saving simulation state with button 14 | - Reading simulation states 15 | - Visaul effect for merging bodies 16 | - Selection buttons for showing force and velocity for each particle 17 | """ 18 | 19 | 20 | class Window: 21 | """ 22 | Handles the main components of a simulation. Contains the game loop and 23 | functionality for panning and drawing lines on the screen. All actual 24 | simulation in handled in the Simulation object, to which pan_offset and 25 | drawn lines are passed. 26 | """ 27 | 28 | def __init__(self, window_x, window_y, background_colour): 29 | pygame.init() 30 | self.window_x = 800 31 | self.window_y = 800 32 | self.font = pygame.font.SysFont('monospace', 15) 33 | self.bg = Color.LGREY 34 | self.screen = pygame.display.set_mode((window_x, window_y)) 35 | self.screen.fill(self.bg) 36 | self.pan_offset = [0,0] 37 | 38 | def main_loop(self, simulation): 39 | """ 40 | Pygame main loop. Uses the ticker class to create consistent timespaces 41 | between ticks. In the main loop: Updates simulation, checks events, 42 | updats statistics on screen, draws screen en increments tick. 43 | """ 44 | running = True 45 | ticker = Ticker(start_time=time.time(), tick_len=1/30) 46 | arrowkey_hold = {pygame.K_LEFT:False, pygame.K_RIGHT:False, pygame.K_UP:False} 47 | 48 | # Main loop 49 | while running: 50 | 51 | # Update simulation 52 | simulation.update_bodies(ticker.i) 53 | self.screen.fill(self.bg) 54 | simulation.draw(self.screen, self.pan_offset, self.bg) 55 | 56 | # Check events 57 | for event in pygame.event.get(): 58 | if event.type == pygame.QUIT: 59 | running = False 60 | if event.type ==pygame.MOUSEBUTTONDOWN: 61 | if event.button == 1: 62 | self.pan(simulation) 63 | if event.button == 3: 64 | self.mouse_draw(simulation) 65 | if event.type == pygame.KEYDOWN: 66 | if event.key in arrowkey_hold: 67 | arrowkey_hold[event.key] = True 68 | if event.type == pygame.KEYUP: 69 | if event.key in arrowkey_hold: 70 | arrowkey_hold[event.key] = False 71 | 72 | # Arrow key hold - for user controlled particle 73 | if arrowkey_hold[pygame.K_LEFT]: 74 | self.key_left(simulation) 75 | if arrowkey_hold[pygame.K_RIGHT]: 76 | self.key_right(simulation) 77 | if arrowkey_hold[pygame.K_UP]: 78 | self.key_up(simulation, 0.15) 79 | 80 | # Tick load stats 81 | stats = ticker.string_stats() 82 | self.display_textlist(stats, Color.DGREY, 15, 5) 83 | bodies_text = f'nr_bodies : {len(simulation.bodies)}' 84 | self.display_text(bodies_text, Color.DGREY, 650, 5) 85 | 86 | # Screen display and next tick 87 | pygame.display.flip() # Draw screen 88 | ticker.next_tick() # Incement tick 89 | 90 | pygame.quit() 91 | 92 | def pan(self, simulation): 93 | """ 94 | Pans the simulation screen using the left mouse button. The x and y 95 | pan_offset is passed to all drawing functions. 96 | """ 97 | initial_offset = self.pan_offset.copy() # Offset when panning is started 98 | pan_start = pygame.mouse.get_pos() # Initial position of mouse 99 | hold = True 100 | while hold: 101 | pan_new = pygame.mouse.get_pos() # Computing pan offset 102 | self.pan_offset[0] = initial_offset[0] + pan_new[0] - pan_start[0] 103 | self.pan_offset[1] = initial_offset[1] + pan_new[1] - pan_start[1] 104 | self.screen.fill(self.bg) 105 | sim.draw(self.screen, self.pan_offset, self.bg) 106 | pygame.display.flip() 107 | for event in pygame.event.get(): # check for mouse release 108 | if event.type == pygame.MOUSEBUTTONUP: 109 | hold = False 110 | 111 | def mouse_draw(self, simulation): 112 | """ 113 | Lets the user draw a line with their rigt mouse button. The line info 114 | is then passed on the the simulation which can use it to create new 115 | particles. 116 | """ 117 | hold = True 118 | duration = 1 # Increases when the mouse is pressed longer 119 | start_pos = pygame.mouse.get_pos() 120 | while hold: 121 | end_pos = pygame.mouse.get_pos() 122 | self.screen.fill(self.bg) 123 | pygame.draw.circle(self.screen, Color.DGREY, start_pos, int(duration)) 124 | pygame.draw.line(self.screen, Color.DGREY, start_pos, end_pos) 125 | sim.draw(self.screen, self.pan_offset, self.bg) 126 | pygame.display.flip() 127 | for event in pygame.event.get(): # check for mouse release 128 | if event.type == pygame.MOUSEBUTTONUP: 129 | hold = False 130 | simulation.user_drawn_particle(start_pos, end_pos, duration, self.pan_offset) 131 | duration += 0.03 132 | 133 | def display_text(self, text, color, x, y): 134 | """ 135 | Draws one line of text on specified coordinates 136 | """ 137 | textblock = self.font.render(text, True, color) 138 | self.screen.blit(textblock, (x,y)) 139 | 140 | def display_textlist(self, text_list, color, x, y): 141 | """ 142 | Draws a list of text 143 | """ 144 | for line in text_list: 145 | self.display_text(line, color, x, y) 146 | y += 15 147 | 148 | def key_left(self, simulation): 149 | simulation.arrowkey_rotation(-1) 150 | 151 | def key_right(self, simulation): 152 | simulation.arrowkey_rotation(1) 153 | 154 | def key_up(self, simulation, force): 155 | simulation.arrowkey_acceleration(force) 156 | 157 | 158 | 159 | # Parameters 160 | x, y = 800, 800 161 | simtype = 3 162 | 163 | # Simulation types 164 | if simtype == 1: 165 | sim = Random_sim(G=0.001) 166 | sim.generate_bodies(nr_planets=5, nr_particles=50, max_pos = [x,y]) 167 | 168 | elif simtype == 2: 169 | sim = Solar_system(G=0.001) 170 | sim.generate_bodies(nr_planets=60, max_pos=[x,y]) 171 | 172 | elif simtype == 3: 173 | sim = User_controlled(G=0.001) 174 | sim.generate_bodies(max_pos=[x,y]) 175 | 176 | 177 | # Running 178 | w = Window(x, y, Color.LGREY) 179 | w.main_loop(sim) 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /Particle.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from math import sin, cos, atan2 3 | from Color import ColorGradient 4 | 5 | 6 | 7 | 8 | class Body: 9 | 10 | def __init__(self, position, mass, color, trail_color, velocity, trail_size): 11 | self.m = mass # Particle mass 12 | self.p = position # Particle postion (unit vector: x,y) 13 | self.v = velocity # Particle velocity (unit vector: x,y) 14 | self.f = [0,0] # Particle net force (unit vector: x,y) 15 | self.rad = max(int(mass ** (1/3)), 1) 16 | self.trail_color = trail_color 17 | self.color = color 18 | self.thickness = self.rad 19 | self.prev_positions = [(position[0], position[1])] 20 | self.trail_size = trail_size 21 | 22 | def update_force(self, bodies, G): 23 | """ 24 | Updates the particles' net force based on the gravitational pull of 25 | each body given in a list, and a gravitational constant. 26 | """ 27 | Fx, Fy = [], [] 28 | for b in bodies: 29 | if not b is self: 30 | x_a, y_a = self.p[0], self.p[1] # Body a position 31 | x_b, y_b = b.p[0], b.p[1] # Body b position 32 | r_ab = ((x_a - x_b)**2 + (y_a - y_b)**2) ** (1/2) # Cartesian distance 33 | m_ab = self.m * b.m # Mass product 34 | rxab = x_b - x_a 35 | ryab = y_b - y_a 36 | Fx.append(G * (m_ab / r_ab**2) * rxab) 37 | Fy.append(G * (m_ab / r_ab**2) * ryab) 38 | self.f[0], self.f[1] = sum(Fx), sum(Fy) 39 | 40 | def update_velocity(self): 41 | """ 42 | Updates the particles' velocity based on the current net force, 43 | divided by its mass. 44 | """ 45 | self.v[0] += self.f[0] / self.m 46 | self.v[1] += self.f[1] / self.m 47 | 48 | def update_position(self): 49 | """ 50 | Offsets the current position with the current velocity. 51 | """ 52 | self.p[0] += self.v[0] 53 | self.p[1] += self.v[1] 54 | 55 | def merge(self, bodies): 56 | """ 57 | Loops over a given list of bodies. If any of these bodies are 58 | within the particles radius, merges the two particles. 59 | When two particles are merged, are their properties are merged as well. 60 | The particle that merges with this particle subsequently gets removed 61 | from the list of bodies. 62 | """ 63 | for b in bodies: 64 | if not b is self: 65 | x_a, y_a = self.p[0], self.p[1] 66 | x_b, y_b = b.p[0], b.p[1] 67 | distance = ((x_a - x_b)**2 + (y_a - y_b)**2) ** (1/2) 68 | if distance < self.rad: 69 | ratio = self.m / (self.m + b.m) 70 | self.v[0] = ratio * self.v[0] + (1-ratio) * b.v[0] 71 | self.v[1] = ratio * self.v[1] + (1-ratio) * b.v[1] 72 | self.m += b.m 73 | self.rad = max(int(self.m ** (1/3)), 1) 74 | bodies.remove(b) 75 | 76 | def bounce(self, xbound, ybound): 77 | """ 78 | Bounces particles that have exceeded the screen boundaries back 79 | into to simulation space. 80 | """ 81 | if self.p[0] > xbound: 82 | self.p[0] = 2 * xbound - self.p[0] 83 | self.v[0] = - self.v[0] 84 | if self.p[1] > ybound: 85 | self.p[1] = 2 * ybound - self.p[1] 86 | self.v[1] = - self.v[1] 87 | if self.p[0] < 0: 88 | self.p[0] = - self.p[0] 89 | self.v[0] = - self.v[0] 90 | if self.p[1] < 0: 91 | self.p[1] = - self.p[1] 92 | self.v[1] = - self.v[1] 93 | 94 | def update_trail(self): 95 | """ 96 | Appends the current position to the trail list. Also 97 | removes segments after lines become too long to reduce computational 98 | load, according to the self.trail_size parameter. 99 | """ 100 | self.prev_positions.append((int(self.p[0]),int(self.p[1]))) 101 | if len(self.prev_positions) > self.trail_size: 102 | self.prev_positions.pop(0) 103 | 104 | def draw_trail(self, screen, pan_offset): 105 | """ 106 | Draws a line connecting all segments of the prev_positions list using 107 | a gradient. 108 | """ 109 | width = int(max(1, self.rad/3)) 110 | g = ColorGradient(self.trail_color, (230, 230, 230), self.trail_size) 111 | positions = list(reversed(self.prev_positions)) 112 | offset_positions = [(x+pan_offset[0], y+pan_offset[1]) for x,y in positions] 113 | for i, p in enumerate(offset_positions[:-1]): 114 | col = g.get_color(i) 115 | pygame.draw.line(screen, col, p, offset_positions[i+1], width) 116 | 117 | def draw_line(self, screen, pan_offset, to_position, color): 118 | """ 119 | Draws a line from the particle to another position. Used for debugging 120 | and visualizing forces or angles. 121 | """ 122 | x = self.p[0] + pan_offset[0] 123 | y = self.p[1] + pan_offset[1] 124 | to_position[0] += pan_offset[0] 125 | to_position[1] += pan_offset[1] 126 | pygame.draw.line(screen, color, (x,y), to_position, 1) 127 | 128 | 129 | 130 | 131 | class Arrow(Body): 132 | """ 133 | Arrow shaped body, usefull to create a visual indication of a particles' 134 | velocity. 135 | """ 136 | 137 | def __init__(self, position, mass, color, trail_color, velocity, trail_size): 138 | super().__init__(position, mass, color, trail_color, velocity, trail_size) 139 | 140 | def draw(self, screen, pan_offset): 141 | """ 142 | Draws an arrow on the particles' coordinates indicating its current 143 | direction. The arrow is defined by X1, Y1 and Y2 which are multiplied 144 | by the particles' radius. 145 | """ 146 | x = self.p[0] + pan_offset[0] 147 | y = self.p[1] + pan_offset[1] 148 | theta = atan2(self.v[1],self.v[0]) 149 | X1, Y1, Y2 = 0.6*self.rad, 1.6*self.rad, 0.3*self.rad 150 | p_top = (int(x + Y1 * cos(theta)), int(y + Y1 * sin(theta))) 151 | p_right = (int(x - X1 * sin(theta)), int(y + X1 * cos(theta))) 152 | p_left = (int(x + X1 * sin(theta)), int(y - X1 * cos(theta))) 153 | p_bottom = (int(x - Y2 * cos(theta)), int(y - Y2 * sin(theta))) 154 | pointslist = (p_top, p_right, p_bottom, p_left) 155 | pygame.draw.polygon(screen, self.color, pointslist) 156 | 157 | 158 | 159 | 160 | class Planet(Body): 161 | """ 162 | Spherical body, useful for representing planets or general particles. 163 | """ 164 | 165 | def __init__(self, position, mass, color, trail_color, velocity, trail_size): 166 | super().__init__(position, mass, color, trail_color, velocity, trail_size) 167 | 168 | def draw(self, screen, pan_offset): 169 | x = self.p[0] + pan_offset[0] 170 | y = self.p[1] + pan_offset[1] 171 | pygame.draw.circle(screen, self.color, (int(x),int(y)), self.rad) 172 | 173 | 174 | 175 | class UserParticle(Body): 176 | """ 177 | Arrow shaped body, usefull to create a visual indication of a particles' 178 | velocity. 179 | """ 180 | 181 | def __init__(self, position, mass, color, trail_color, velocity, trail_size): 182 | super().__init__(position, mass, color, trail_color, velocity, trail_size) 183 | self.angle = 0 184 | 185 | def draw(self, screen, pan_offset): 186 | """ 187 | Draws an arrow on the particles' coordinates indicating its current 188 | direction. The arrow is defined by X1, Y1 and Y2 which are multiplied 189 | by the particles' radius. 190 | """ 191 | x = self.p[0] + pan_offset[0] 192 | y = self.p[1] + pan_offset[1] 193 | theta = self.angle 194 | X1, Y1, Y2 = 0.6*self.rad, 1.6*self.rad, 0.3*self.rad 195 | p_top = (int(x + Y1 * cos(theta)), int(y + Y1 * sin(theta))) 196 | p_right = (int(x - X1 * sin(theta)), int(y + X1 * cos(theta))) 197 | p_left = (int(x + X1 * sin(theta)), int(y - X1 * cos(theta))) 198 | p_bottom = (int(x - Y2 * cos(theta)), int(y - Y2 * sin(theta))) 199 | pointslist = (p_top, p_right, p_bottom, p_left) 200 | pygame.draw.polygon(screen, self.color, pointslist) 201 | 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # N_body_simulation 2 | 3 | * Main: contains the Pygame main loop. The simulation to be used can be selected at the bottom. 4 | * Particle: contains the particle classes. 5 | * Simulations: new simulations can easily be added here. 6 | * Color: some constants and functions to help with colors and gradients. 7 | * Ticker: controls the iterations of the Pygame main loop. 8 | 9 | # Usage 10 | 11 | * Run the main.py file 12 | * Left mouse pans the screen 13 | * Right mouse (hold) creates new particles in the direction indicated by the line. Longer hold increases the particles' mass. -------------------------------------------------------------------------------- /Simulations.py: -------------------------------------------------------------------------------- 1 | from random import randint, uniform 2 | from Color import Color 3 | from Particle import Arrow, Planet, UserParticle 4 | from math import sqrt, sin, cos 5 | 6 | 7 | class N_Body: 8 | """ 9 | Simulation parent class. All other classes inherit all functionality from 10 | this class, with the exception of different generate_bodies functions, 11 | which determines the actual bodies in the simulation. New simulations can 12 | be added by just inhereting this class, and importing them to main.py. 13 | """ 14 | 15 | def __init__(self, G): 16 | self.G = G 17 | 18 | def update_bodies(self, iteration): 19 | """ 20 | Updates all bodies in the simulation by merging them, computing the 21 | net force, updating velocity and updating postions. The trail is not 22 | updated every iteration to save computational load. 23 | """ 24 | for p in self.bodies: 25 | p.merge(self.bodies) 26 | p.update_force(self.bodies, self.G) 27 | p.update_velocity() 28 | p.update_position() 29 | if iteration % 4 == 0: 30 | p.update_trail() 31 | 32 | def draw(self, screen, pan_offset, background_colour): 33 | """ 34 | Draws the simulation bodies on the Pygame screen. 35 | """ 36 | for p in self.bodies: 37 | p.draw_trail(screen, pan_offset) 38 | for p in self.bodies: 39 | p.draw(screen, pan_offset) 40 | 41 | def user_drawn_particle(self, start_pos, end_pos, duration, pan_offset): 42 | """ 43 | Function that can utilize a line drawn in the simulation. This line, 44 | as well as the duration of the mouse press are used to create a new 45 | body, velocity and mass. 46 | """ 47 | vx = 0.02 * (start_pos[0] - end_pos[0]) 48 | vy = 0.02 * (start_pos[1] - end_pos[1]) 49 | mass = duration ** 3 50 | x = start_pos[0] - pan_offset[0] 51 | y = start_pos[1] - pan_offset[1] 52 | p = Planet([x,y], mass, Color.RED, Color.MGREY, [vx,vy], 20) 53 | self.bodies.append(p) 54 | 55 | def arrowkey_rotation(self, direction): 56 | """ 57 | Function can be overriden in simulations to rotate user controlled 58 | particles 59 | """ 60 | pass 61 | 62 | def arrowkey_acceleration(self, force): 63 | """ 64 | Function can be overriden in simulations to accelerate user controlled 65 | particles 66 | """ 67 | pass 68 | 69 | 70 | 71 | 72 | 73 | class Random_sim(N_Body): 74 | """ 75 | Simple simulation with a number of random arrows, with little mass, and 76 | planets, with larger mass. 77 | """ 78 | 79 | def __init__(self, G): 80 | super().__init__(G) 81 | 82 | def generate_bodies(self, nr_planets, nr_particles, max_pos): 83 | self.bodies = [] 84 | for i in range(nr_particles): 85 | position = [randint(0,max_pos[0]),randint(0,max_pos[1])] 86 | mass = randint(1,200) 87 | color = Color.random_vibrant() 88 | trail_color = Color.MGREY 89 | velocity = [uniform(-2,2),uniform(-2,2)] 90 | trail_size = 30 91 | p = Arrow(position, mass, color, trail_color, velocity, trail_size) 92 | self.bodies.append(p) 93 | 94 | for i in range(nr_planets): 95 | position = [randint(0,max_pos[0]),randint(0,max_pos[1])] 96 | mass = randint(200,1000) 97 | color = Color.DGREY 98 | trail_color = Color.MGREY 99 | velocity = [uniform(-1,1),uniform(-1,1)] 100 | trail_size = 200 101 | p = Planet(position, mass, color, trail_color, velocity, trail_size) 102 | self.bodies.append(p) 103 | 104 | 105 | 106 | 107 | 108 | class Solar_system(N_Body): 109 | """ 110 | Simulation with a central 'sun' and numerous planets rotating around it. 111 | """ 112 | 113 | def __init__(self, G): 114 | super().__init__(G) 115 | 116 | def generate_bodies(self, nr_planets, max_pos): 117 | self.bodies = [] 118 | 119 | # Simulation starting midpoint of screen 120 | mid = [int(max_pos[0]/2), int(max_pos[1]/2)] 121 | 122 | # Creating sun 123 | sun = Planet(position = mid, mass = 30000, color = Color.GOLD, 124 | trail_color = Color.MGREY, velocity = [0,0], trail_size = 50) 125 | self.bodies.append(sun) 126 | 127 | for i in range(nr_planets): 128 | 129 | # Random parameters 130 | x, y = randint(0, max_pos[0]), randint(0, max_pos[1]) 131 | mass = randint(1,150) 132 | 133 | # Unit vector pointing at the sun 134 | dis_x = mid[0] - x 135 | dis_y = mid[0] - y 136 | dis = sqrt(dis_x**2 + dis_y**2) 137 | sun_normalized_x = dis_x / dis 138 | sun_normalized_y = dis_y / dis 139 | 140 | # Velocity unit vector, perpendicular to sun vector 141 | vel_x = sun_normalized_y 142 | vel_y = - sun_normalized_x 143 | 144 | # Velocity: velocity unit vector scaled with distance, G, sun mass and constant 145 | combined_mass = self.bodies[0].m + mass 146 | vx = 3.8 * sqrt(self.G * combined_mass / sqrt(dis)) * vel_x 147 | vy = 3.8 * sqrt(self.G * combined_mass / sqrt(dis)) * vel_y 148 | 149 | # Creating planet 150 | position = [x, y] 151 | color = Color.random_dull() 152 | trail_color = color 153 | velocity = [vx, vy] 154 | trail_size = 20 155 | p = Planet(position, mass, color, trail_color, velocity, trail_size) 156 | self.bodies.append(p) 157 | 158 | 159 | 160 | 161 | 162 | class User_controlled(N_Body): 163 | """ 164 | Simulation with a user controlled particle. 165 | """ 166 | 167 | def __init__(self, G): 168 | super().__init__(G) 169 | 170 | def generate_bodies(self, max_pos): 171 | self.bodies = [] 172 | 173 | # Create user controlled particle 174 | mid = [int(max_pos[0]/2), int(max_pos[1]/2)] 175 | self.u = UserParticle(mid, 500, Color.NAVY, Color.NAVY, [0,0], 20) 176 | self.bodies.append(self.u) 177 | 178 | def arrowkey_rotation(self, direction): 179 | """ 180 | Function can be overriden in simulations to rotate user controlled 181 | particles 182 | """ 183 | self.u.angle += 0.2 * direction 184 | 185 | def arrowkey_acceleration(self, force): 186 | """ 187 | Function can be overriden in simulations to accelerate user controlled 188 | particles 189 | """ 190 | normalized_x = cos(self.u.angle) 191 | normalized_y = sin(self.u.angle) 192 | self.u.v[0] += normalized_x * force 193 | self.u.v[1] += normalized_y * force 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /Ticker.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class Ticker: 5 | 6 | def __init__(self, start_time, tick_len): 7 | self.start_time = start_time 8 | self.tick_start = start_time 9 | self.tick_len = tick_len 10 | 11 | # Initializing Tick load statistics 12 | self.i = 0 13 | self.load_list = [0 for i in range(20)] # Contains the 20 last tick load values 14 | self.avg_load = 0 15 | self.max_load = 0 16 | self.min_load = 0 17 | self.run_time = 0 18 | self.iter_len = 0 19 | 20 | def next_tick(self): 21 | """ 22 | Records the used computational time of tick. Then pauses the programm 23 | until the time until the next tick has elapsed. 24 | """ 25 | t = time.time() - self.tick_start # Elapsed time of tick 26 | self.update_statistics(t) # Update tick stats 27 | time.sleep(max(0, self.tick_len - t)) # Wait until next tick start 28 | self.tick_start = time.time() # Start of next tick 29 | 30 | def update_statistics(self, t): 31 | """ 32 | Updates the tick statistics for the current tick. 33 | """ 34 | load = round(100 * t / self.tick_len, 2) # Compute tick load 35 | self.load_list.pop(0) # Remove last load from list 36 | self.load_list.append(load) # Append load to list 37 | self.i += 1 # Iteration number 38 | self.avg_load = round(sum(self.load_list) / len(self.load_list),2) 39 | self.max_load = round(max(self.load_list),2) 40 | self.min_load = round(min(self.load_list),2) 41 | self.run_time = max(0.01, round(time.time() - self.start_time,2)) 42 | self.iter_len = round(self.i / self.run_time, 2) 43 | 44 | def string_stats(self): 45 | """ 46 | Returns all current stats values as a list of printeable strings 47 | """ 48 | return [f'avg_load: {self.avg_load}', 49 | f'max_load: {self.max_load}', 50 | f'min_load: {self.min_load}', 51 | f'run_time: {self.run_time}', 52 | f'iter_len: {self.iter_len}'] 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | --------------------------------------------------------------------------------