I am trying to code an N-body simulation code in python and have successfully managed to produce a system involving the Sun, Earth and Jupiter as below using a leapfrog approximation method.
However, when I try and extend the same code for N bodies of equal mass all with zero velocity, I don't get the expected result of a system forming. Instead, the following is produced where the bodies spread out after initially being attracted to each other.
This same pattern is replicated regardless of the number of initial particles used.
This second image is just an enlarged version of the first showing they are initially attracted to each other.
Leading me to believe the error must lie in my initial conditions:
N = 3
mass = 1e30
R = 1e10
V = np.zeros([N,3])
M = np.full([N],mass)
P = np.random.uniform(-R, R, (N,3))
epsilon = 0.1 * R
acceleration calculation:
def calc_acceleration(position, mass, softening):
G = 6.67 * 10**(-11)
N = position.shape[0] # N = number of rows in particle_positions array
acceleration = np.zeros([N,3])
#print(N)
for i in range(N):
#print(i)
for j in range(N):
if i != j:
#print("j", j)
dx = position[i,0] - position[j,0]
dy = position[i,1] - position[j,1]
dz = position[i,2] - position[j,2]
#print(dx,dy,dz)
inv_r3 = ((dx**2 + dy**2 + dz**2 + softening**2)**(-1.5))
acceleration[i,0] += - G * mass[j] * dx * inv_r3
acceleration[i,1] += - G * mass[j] * dy * inv_r3
acceleration[i,2] += - G * mass[j] * dz * inv_r3
return(acceleration)
leap frog functions:
def calc_next_v_half(position, mass, velocity, softening, dt):
half_velocity = np.zeros_like(velocity)
half_velocity = velocity + calc_acceleration(position, mass, softening) * dt/2
return(half_velocity)
def calc_next_position(position, mass, velocity, dt):
next_position = np.zeros_like(position)
next_position = position + velocity * dt
return(next_position)
actual program function:
def programe(position, mass, velocity, softening, time, dt):
no_of_time_steps = (round(time/dt))
all_positions = np.full((no_of_time_steps, len(mass), 3), 0.0)
all_velocities = []
kinetic_energy = []
potential_energy = []
total_energy = []
for i in range(no_of_time_steps):
all_positions[i] = position
all_velocities.append(velocity)
'leap frog'
velocity = calc_next_v_half(position, mass, velocity, softening, dt)
position = calc_next_position(position, mass, velocity, dt)
velocity = calc_next_v_half(position, mass, velocity, softening, dt)
return(all_positions, all_velocities, kinetic_energy, potential_energy, total_energy)
The problem is that the symplectic methods only have their special properties as long as the systems stays well away from any singularities. For a gravitational system this is the case if it is hierarchical like in a solar system with sun, planets and moons where all orbits have low eccentricities.
However, if you consider a "star cluster" with objects of about equal mass, you do not get Kepler ellipses and the likelihood for very close encounters becomes rather high. The more so as your initial condition of zero velocity results in an initial free fall of all stars towards the common center of gravity, as can also be seen in your detail picture.
Due to the potential energy falling down into a singularity, the kinetic energy increases as the distance decreases, so close encounters equal high speed. With a constant step size like in the leapfrog-Verlet method, the sampling rate becomes too small to represent the curve, capture the swing-by fully. Energy conservation is grossly violated and the high speed is kept beyond the close encounter, leading to the unphysical explosion of the system.
Related
Probably a bit more of a physics question than a programming question...
Basically I am trying to simulate a robot with a torsion spring in its knee (see picture) and measure what forces are applied to the knee joint and how much the spring deforms.
picture of robot
The program I've written produces graphs that have the correct shape, but for some reason the angle of the spring seems to be scaling with the size of my timesteps, which is definitely not correct.
I have attached my code below, but here is also a detailed explanation of what I think I am doing:
I assume that besides gravitation, there is only one force ever applied to the robot, and that force is applied at the start for a certain duration and is aimed straight down. From that force I calculate the angular velocity of my joint.
After that, I enter a loop where in every timestep I update the forces and the angle of the spring. I calculate the gravitational force dependent on the angle. I assume that my model resembles an inverted pendulum, and that the term for the mass of the leg is negligible. Then I calculate the counterforce exerted by the spring, and get the total force from the difference between the two.
I calculate the velocity change from the force, and update the velocity. From the velocity I get the change in angle and update the angle as well. I repeat that loop until a time limit is reached.
The rest of the code is just for plotting.
For some reason, the scale of the angle changes drastically with the size of the timesteps I choose. Other than that, however, the graphs look as I would expect: Oscillating briefly and then settling on one value. One problem I see is that the force of the spring directly depends on the timestep, while the gravitational force does not. Since everything else depends on the difference between the two forces, this changes the scale. However, I do not know how to fix this.
Thanks for any help!
import math
from scipy import constants
import matplotlib.pyplot as plt
class Spring:
def __init__(self, k, l=0.17, m=6.0):
self.k = k
self.l = l
self.m = m
self.alphaplt = []
self.forceplt = []
def impulse(self, force, duration, at_angle, stept):
time = 0
alpha = at_angle
i = self.m * self.l ** 2
vk = (force / self.m * duration) / self.l
while time <= 100:
tm = 0.5 * self.m * constants.g * self.l * math.sin(alpha)
tf = self.k * vk * stept / self.l
tg = tm - tf
vk = vk + (tg * stept * self.l) / i
alpha = alpha + vk * stept
time += stept
self.alphaplt.append(alpha)
self.forceplt.append(tg)
plt.plot(self.alphaplt)
plt.show()
plt.plot(self.forceplt)
plt.show()
if __name__ == "__main__":
s = Spring(0.75)
s.impulse(90, 0.5, 0, 0.01)
If you want to see an animated simulation of this system, go to
Online VPython's webpage, register (it's free), paste the script I have written below and run it. Some explanations of the code are written included as comments in the script
Web VPython 3.2
# system of differential equations of the mathematical model of the physical system:
def f(state, param, F):
m, L, k, theta_0, g = param
a = state.x
p = state.y
s_a = sin(a)
c_a = cos(a)
da_dt = p / (s_a**2)
dp_dt = c_a * p**2 / (s_a**3) - (k/(2*m*L**2)) * (2*a + theta_0 - pi) + ((g + F/m) / (2*L)) * s_a
return vector(da_dt, dp_dt, 0.)
# one Runge-Kutta time-step propagation of the system's dynamics:
def propagate_RK4(f, state, param, F, dt):
k1 = f(state, param, F)
k2 = f(state + dt*k1/2., param, F)
k3 = f(state + dt*k2/2., param, F)
k4 = f(state + dt*k3, param, F)
return state + dt*(k1 + 2*k2 + 2*k3 + k4) / 6.
# transforming the dynamical variable angle into the actual, physical configuration of the system
def configure(struct, state):
struct[0].pos = vector( L*sin(state.x), L*cos(state.x), 0. )
struct[1].pos = vector( 0., 2*L*cos(state.x), 0. )
struct[2].axis = struct[0].pos
struct[3].pos = struct[0].pos
struct[3].axis = struct[1].pos - struct[0].pos
return None
# initialization and animation of the system's dynamics:
# constant parameters of the model:
m=6.0
L=0.17
k=55.75
theta_0 = 2*pi/3
g = 9.8
F = 150.
param = (m, L, k, theta_0, g)
a_0 = pi/10
# fixed location of the lowest, ground level, joint:
sphere(pos = vector(0., 0., 0.), radius = .005, color=color.red)
# initial positions of the moving middle joint, where the torsion spring is
joint_1 = sphere(pos = vector(L*sin(a_0), L*cos(a_0), 0.), radius = .01, color=color.red, make_trail=True)
# initial positions of the upper joint, where the main load (box) is positioned
joint_2 = sphere(pos = vector(0., 2*L*cos(a_0), 0. ), radius = .01, color=color.red, make_trail=True)
# initial configuration of the bar between ground joint and joint 1
bar_01 = cylinder(pos=vector(0,0,0), axis=vector(0, L,0), radius=0.003, color=color.orange)
# initial configuration of the bar between ground joint and joint 2
bar_12 = cylinder(pos=vector(0,L,0), axis=vector(0,L,0), radius=0.003, color=color.orange)
# initial state of the system, given by an initial angle a_0:
state = vector(a_0, 0., 0.)
t = 0.
dt = 0.01
force_duration = 0.7
frame_rate = 15
force_duration = force_duration / frame_rate
# drawing in yellow the locations of joint 1 and 2 at time t=0
sphere(pos = vector(L*sin(a_0), L*cos(a_0), 0.), radius = .005, color=color.yellow)
sphere(pos = vector(0., 2*L*cos(a_0), 0. ), radius = .005, color=color.yellow)
# animation of the dynamics of the system while the constant external force is acting:
while t <= force_duration:
rate(frame_rate)
state = propagate_RK4(f, state, param, F, dt)
configure([joint_1, joint_2, bar_01, bar_12], state)
t=t+dt
# drawing in yellow the locations of joint 1 and 2 at the time moment when the external force stops acting
sphere(pos = vector(L*sin(state.x), L*cos(state.x), 0.), radius = .005, color=color.yellow)
sphere(pos = vector(0., 2*L*cos(state.x), 0. ), radius = .005, color=color.yellow)
# animation of the dynamics of the system after the constant external force has stopped acting:
while force_duration < t <= 10:
rate(frame_rate)
state = propagate_RK4(f, state, param, 0, dt)
configure([joint_1, joint_2, bar_01, bar_12], state)
t=t+dt
I am trying to simulate the orbit of a planet around a star using the Runge-Kutta 4 method. After speaking to tutors my code should be correct. However, I am not generating my expected 2D orbital plot but instead a linear plot. This is my first time using solve_ivp to solve a second order differential. Can anyone explain why my plots are wrong?
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
# %% Define derivative function
def f(t, z):
x = z[0] # Position x
y = z[1] # Position y
dx = z[2] # Velocity x
dy = z[3] # Velocity y
G = 6.674 * 10**-11 # Gravitational constant
M = 2 # Mass of binary stars in solar masses
c = 2*G*M
r = np.sqrt(y**2 + x**2) # Distance of planet from stars
zdot = np.empty(6) # Array for integration solutions
zdot[0] = x
zdot[1] = y
zdot[2] = dx # Velocity x
zdot[3] = dy #Velocity y
zdot[4] = (-c/(r**3))*(x) # Acceleration x
zdot[5] = (-c/(r**3))*(y) # Acceleration y
return zdot
# %% Define time spans, initial values, and constants
tspan = np.linspace(0., 10000., 100000000)
xy0 = [0.03, -0.2, 0.008, 0.046, 0.2, 0.3] # Initial positions x,y in R and velocities
# %% Solve differential equation
sol = solve_ivp(lambda t, z: f(t, z), [tspan[0], tspan[-1]], xy0, t_eval=tspan)
# %% Plot
#plot
plt.grid()
plt.subplot(2, 2, 1)
plt.plot(sol.y[0],sol.y[1], color='b')
plt.subplot(2, 2, 2)
plt.plot(sol.t,sol.y[2], color='g')
plt.subplot(2, 2, 3)
plt.plot(sol.t,sol.y[4], color='r')
plt.show()
With the ODE function as given, you are solving in the first components the system
xdot = x
ydot = y
which has well-known exponential solutions. As the exponential factor is the same long both solutions, the xy-plot will move along a line through the origin.
The solution is of course to fill zdot[0:2] with dx,dy, and zdot[2:4] with ax,ay or ddx,ddy or however you want to name the components of the acceleration. Then the initial state also has only 4 components. Or you need to make and treat position and velocity as 3-dimensional.
You need to put units to your constants and care that all use the same units. G as cited is in m^3/kg/s^2, so that any M you define will be in kg, any length is in m and any velocity in m/s. Your constants might appear ridiculously small in that context.
It does not matter what the comment in the code says, there will be no magical conversion. You need to use actual conversion computations to get realistic numbers. For instance using the numbers
G = 6.67408e-11 # m^3 s^-2 kg^-1
AU = 149.597e9 # m
Msun = 1.988435e30 # kg
hour = 60*60 # seconds in an hour
day = hour * 24 # seconds in one day
year = 365.25*day # seconds in a year (not very astronomical)
one could guess that for a sensible binary system of two stars of equal mass one has
M = 2*Msun # now actually 2 sun masses
x0 = 0.03*AU
y0 = -0.2*AU
vx0 = 0.008*AU/day
vy0 = 0.046*AU/day
For the position only AU makes sense as unit, the speed could also be in AU/hour. By https://math.stackexchange.com/questions/4033996/developing-keplers-first-law and Cannot get RK4 to solve for position of orbiting body in Python the speed for a circular orbit of radius R=0.2AU around a combined mass of 2*M is
sqrt(2*M*G/R)=sqrt(4*Msun*G/(0.2*AU)) = 0.00320 * AU/hour = 0.07693 AU/day
which is ... not too unreasonable if the given speeds are actually in AU/day. Invoke the computations from https://math.stackexchange.com/questions/4050575/application-of-the-principle-of-conservation to compute if the Kepler ellipse would look sensible
r0 = (x0**2+y0**2)**0.5
dotr0 = (x0*vx0+y0*vy0)/r0
L = x0*vy0-y0*vx0 # r^2*dotphi = L constant, L^2 = G*M_center*R
dotphi0 = L/r0**2
R = L**2/(G*2*M)
wx = R/r0-1; wy = -dotr0*(R/(G*2*M))**0.5
E = (wx*wx+wy*wy)**0.5; psi = m.atan2(wy,wx)
print(f"half-axis R={R/AU} AU, eccentr. E={E}, init. angle psi={psi}")
print(f"min. rad. = {R/(1+E)/AU} AU, max. rad. = {R/(1-E)/AU} AU")
which returns
half-axis R=0.00750258 AU, eccentr. E=0.96934113, init. angle psi=3.02626659
min. rad. = 0.00380969 AU, max. rad. = 0.24471174 AU
This gives an extremely thin ellipse, which is not that astonishing as the initial velocity points almost directly to the gravity center.
orbit variants with half-day steps marked, lengths in AU
If the velocity components were swapped one would get
half-axis R=0.07528741 AU, eccentr. E=0.62778767, init. angle psi=3.12777251
min. rad. = 0.04625137 AU, max. rad. = 0.20227006 AU
This is a little more balanced.
I would like to make some kind of solar system in pygame. I've managed to do a fixed one but I thought it would be more interesting to do one with planets moving around the sun and moons around planets etc. Is there a way I could do that (using pygame if possible)?
What I would like is :
Sun = pygame.draw.circle(...)
planet1 = pygame.draw.circle(...)
etc.
a = [planet1, planet2, ...]
for p in a:
move p[2] to pos(x, y)
That is what I think would work but I'm not sure how to do it. Also, I've thought about deleting the ancient planet and drawing a new one right next to it, but problem is I'm using random features (like colours, distance to the sun, number of planets in the system etc.) and it would have to keep these same features. Any ideas?
Thanks in advance!
You can implement gravity with Newton's Law of Universal Gravitation and Newton's Second Law to get the accelerations of the planets. Give each planet an initial position, velocity and mass. Acceleration is change in velocity a = v * dt, velocity is change in position v = r * dt, so we can integrate to find velocity and position.
Universal gravitation: F = G * m1 * m2 / r ** 2 where F is the magnitude of the force on the object, G is the gravitational constant, m1 and m2 are the masses of the objects and r is the distance between the two objects.
Newton's Second Law: F = m1 * a where a is the acceleration.
dt = 0.01 # size of time step
G = 100 # gravitational constant
def calcGravity(sun, planet):
'Returns acceleration of planet with respect to the sun'
diff_x = sun.x - planet.x
diff_y = sun.y - planet.y
acceleration = G * sun.mass / (diff_x ** 2 + diff_y ** 2)
accel_x = acceleration * diff_x / (diff_x ** 2 + diff_y ** 2)
accel_y = acceleration * diff_y / (diff_x ** 2 + diff_y ** 2)
return accel_x, accel_y
while True:
# update position based on velocity
planet.x += planet.vel_x * dt
planet.y += planet.vel_y * dt
# update velocity based on acceleration
accel_x, accel_y = calcGravity(sun, planet)
planet.vel_x += accel_x * dt
planet.vel_y += accel_y * dt
This can produce circular and elliptical orbits. Creating an orbiting moon requires a very small timestep (dt) for the numeric integration.
Note: this approach is subtly inaccurate due to the limits of numeric integration.
Sample implementation in pygame here, including three planets revolving around a sun, a moon, and a basic orbital transfer.
https://github.com/c2huc2hu/orbital_mechanics
Coordinates of a planet rotated about the Sun through some angle with respect to the X-axis are , where r is the distance to the Sun, theta is that angle, and (a, b) are the coordinates of the sun. Draw your circle centered at (x, y).
EDIT:
General elliptical orbit:
Where
r0 is the radius of a circular orbit with the same angular momentum, and e is the "eccentricity" of the ellipse
I have the following code. This code is simulation of orbiting objects around other objects, E.g. Solar system. As you run it, the objects orbit in circular trajectory.
import math
from vpython import *
lamp = local_light(pos=vector(0,0,0), color=color.yellow)
# Data in units according to the International System of Units
G = 6.67 * math.pow(10,-11)
# Mass of the Earth
ME = 5.973 * math.pow(10,24)
# Mass of the Moon
MM = 7.347 * math.pow(10,22)
# Mass of the Mars
MMa = 6.39 * math.pow(10,23)
# Mass of the Sun
MS = 1.989 * math.pow(10,30)
# Radius Earth-Moon
REM = 384400000
# Radius Sun-Earth
RSE = 149600000000
RMS = 227900000000
# Force Earth-Moon
FEM = G*(ME*MM)/math.pow(REM,2)
# Force Earth-Sun
FES = G*(MS*ME)/math.pow(RSE,2)
# Force Mars-Sun
FEMa = G*(MMa*MS)/math.pow(RMS,2)
# Angular velocity of the Moon with respect to the Earth (rad/s)
wM = math.sqrt(FEM/(MM * REM))
# Velocity v of the Moon (m/s)
vM = wM * REM
print("Angular velocity of the Moon with respect to the Earth: ",wM," rad/s")
print("Velocity v of the Moon: ",vM/1000," km/s")
# Angular velocity of the Earth with respect to the Sun(rad/s)
wE = math.sqrt(FES/(ME * RSE))
# Angular velocity of the Mars with respect to the Sun(rad/s)
wMa = math.sqrt(FEMa/(MMa * RMS))
# Velocity v of the Earth (m/s)
vE = wE * RSE
# Velocity v of the Earth (m/s)
vMa = wMa * RMS
print("Angular velocity of the Earth with respect to the Sun: ",wE," rad/s")
print("Velocity v of the Earth: ",vE/1000," km/s")
# Initial angular position
theta0 = 0
# Position at each time
def positionMoon(t):
theta = theta0 + wM * t
return theta
def positionMars(t):
theta = theta0 + wMa * t
return theta
def positionEarth(t):
theta = theta0 + wE * t
return theta
def fromDaysToS(d):
s = d*24*60*60
return s
def fromStoDays(s):
d = s/60/60/24
return d
def fromDaysToh(d):
h = d * 24
return h
# Graphical parameters
print("\nSimulation Earth-Moon-Sun motion\n")
days = 365
seconds = fromDaysToS(days)
print("Days: ",days)
print("Seconds: ",seconds)
v = vector(384,0,0)
E = sphere(pos = vector(1500,0,0), color = color.blue, radius = 60, make_trail=True)
Ma = sphere(pos = vector(2300,0,0), color = color.orange, radius = 30, make_trail=True)
M = sphere(pos = E.pos + v, color = color.white,radius = 10, make_trail=True)
S = sphere(pos = vector(0,0,0), color = color.yellow, radius=700)
t = 0
thetaTerra1 = 0
dt = 5000
dthetaE = positionEarth(t+dt)- positionEarth(t)
dthetaM = positionMoon(t+dt) - positionMoon(t)
dthetaMa = positionMars(t+dt) - positionMars(t)
print("delta t:",dt,"seconds. Days:",fromStoDays(dt),"hours:",fromDaysToh(fromStoDays(dt)),sep=" ")
print("Variation angular position of the Earth:",dthetaE,"rad/s that's to say",degrees(dthetaE),"degrees",sep=" ")
print("Variation angular position of the Moon:",dthetaM,"rad/s that's to say",degrees(dthetaM),"degrees",sep=" ")
while t < seconds:
rate(500)
thetaEarth = positionEarth(t+dt)- positionEarth(t)
thetaMoon = positionMoon(t+dt) - positionMoon(t)
thetaMars = positionMars(t+dt) - positionMars(t)
# Rotation only around z axis (0,0,1)
E.pos = rotate(E.pos,angle=thetaEarth,axis=vector(0,1,0))
Ma.pos = rotate(Ma.pos,angle=thetaMars,axis=vector(0,1,0))
v = rotate(v,angle=thetaMoon,axis=vector(0,1,0))
M.pos = E.pos + v
t += dt
I am wondering How to change the path of orbit to elliptical?
I have tried several ways but I could not manage to find any solution.
Thank you.
Thank you
This seems like more of a physics issue as opposed to a programming issue. The problem is that you are assuming that each of the orbits are circular when calculating velocity and integrating position linearly (e.g v * dt). This is not how you would go about calculating the trajectory of an orbiting body.
For the case of simplicity, we will assume all the masses are point masses so there aren't any weird gravity gradients or attitude dynamics to account for.
From there, you can refer to this MIT page. (http://web.mit.edu/12.004/TheLastHandout/PastHandouts/Chap03.Orbital.Dynamics.pdf) on orbit dynamics. On the 7th page, there is an equation relating the radial position from your centerbody as a function of a multitude of orbital parameters. It seems like you have every parameter except the eccentricity of the orbit. You can either look that up online or calculate it if you have detailed ephemeral data or apoapsis/periapsis information.
From that equation, you will see a phi - phi_0 term in the denominator. That is colloquially known as the true anomaly of the satellite. Instead of time, you would iterate on this true anomaly parameter from 0 to 360 to find your radial distance, and from true anomaly, inclination, right angle to the ascending node, and the argument of periapses, you can find the 3D cartesian coordinates at a specific true anomaly.
Going from true anomaly is a little less trivial. You will need to find the eccentric anomaly and then the mean anomaly at each eccentric anomaly step. You now have mean anomaly as a function of time. You can linearly interpolate between "nodes" at which you calculate the position with v * dt. You can calculate the velocity from using the vis-viva equation and dt would be the difference between the calculated time steps.
At each time step you can update the satellite's position in your python program and it will properly draw your trajectories.
For more information of the true anomaly, wikipedia has a good description of it: https://en.wikipedia.org/wiki/True_anomaly
For more information about orbital elements (which are needed to convert from radial position to cartesian coordinates): https://en.wikipedia.org/wiki/Orbital_elements
I know how to calculate the scalar of the velocity vector after a collision with 2 circles
(as per this link: https://gamedevelopment.tutsplus.com/tutorials/how-to-create-a-custom-2d-physics-engine-the-basics-and-impulse-resolution--gamedev-6331)
These circles cannot rotate and do not have friction but can have different masses, however I cannot seem to find out any way to find the unit vector that I need to multiply the scalar of velocity by to get the new velocity of the particles after the collision.
I also know how to check if 2 circles are colliding.
Also, I am only dealing with this in a purely "maths-sense" (ie. the circles have a center and a radius), and would like to know how I can represent these circles on the screen in python 3.0.
The vector class:
class Vector():
def __init__(self,x,y):
self.x = x
self.y = y
def add(self, newVector):
return Vector(self.x+newVector.x, self.y+newVector.y)
def subtract(self,newVector):
return Vector(self.x-newVector.x, self.y-newVector.y)
def equals(self, newVector):
return Vector(newVector.x,newVector.y)
def scalarMult(self, scalar):
return Vector(self.x*scalar, self.y*scalar)
def dotProduct(self, newVector):
return (self.x*newVector.x)+(self.y*newVector.y
def distance(self):
return math.sqrt((self.x)**2 +(self.y)**2)
The circle class:
class Particles():
def __init__(self,currentPos, oldPos, accel, dt,mass, center, radius):
self.currentPos = currentPos
self.oldPos = oldPos
self.accel = accel
self.dt = dt
self.mass = mass
self.center = center
self.radius = radius
def doVerletPosition(currentPos, oldPos, accel, dt):
a = currentPos.subtract(oldPos)
b = currentPos.add(a)
c = accel.scalarMult(dt)
d = c.scalarMult(dt)
return d.add(b)
def doVerletVelocity(currentPos, oldPos, dt):
deltaD = (currentPos.subtract(oldPos))
return deltaD.scalarMult(1/dt)
def collisionDetection(self, center, radius):
xCenter = (self.radius).xComponent()
yCenter = (self.radius).yComponent()
xOther = radius.xComponent()
yOther = radius.yComponent()
if ((xCenter - xOther)**2 + (yCenter-yOther)**2 < (self.radius + radius)**2):
return True
else:
return False
I do know about AABBs, but I am only using around 10 particles for now, and AABBs are not necessary now.
You know that the force transmitted between the two discs has to go along the "normal vector" for this collision, which is easy to get - it's just the vector along the line connecting the centers of the two discs.
You have four constraints: conservation of momentum (which counts for two constraints since it applies in x and y), conservation of energy, and this "force along normal" constraint. And you have four unknowns, namely the x and y components of the final velocities. Four equations and four unknowns, you can solve for your answer. Due to my background in physics, I've written this out in terms of momentum instead of velocity, but hopefully that's not too hard to parse. (Note, for instance, that kinetic energy is equal to p**2/2m or 1/2 mv**2.)
## conservation of momentum
p_1_x_i + p_2_x_i = p_1_x_f + p_2_x_f ## p_1_x_i := momentum of disc _1_ in _x_ axis intially _i
p_1_y_i + p_2_x_i = p_1_y_f + p_2_y_f
## conservation of energy
(p_1_x_i**2 + p_1_y_i**2)/(2*m_1) + (p_2_x_i**2 + p_2_y_i**2)/(2*m_2) = (p_1_x_f**2 + p_1_y_f**2)/(2*m_1) + (p_2_x_f**2 + p_2_y_f**2)/(2*m_2)
## impulse/force goes along the normal vector
tan(th) := (x_2-x_1)/(y_2-y_1) # tangent of the angle of the collision
j_1_x := p_1_x_i - p_1_x_f # change in momentum aka impulse
j_1_y := p_1_y_i - p_1_y_f
tan(th) = -j_1_x/j_1_y
(I hope the notation is clear. It would be much clearer if I could use latex, but stackoverflow doesn't support it.)
Hope this helps!