Gravity Problems - python

I've been trying to make a code with pygame to simulate simple gravity. At the moment, there is only one object (HOM) which is orbiting the sun. However, for reasons unknown to me, whenever I run the code, HOM travels round the sun in an orbit at the start, but then accelerates away from the sun when it reaches ~135 degrees from vertical.
Does anyone know why this is happening and how I can fix it? I have been printing some variables to try and source the problem, but have had no luck so far.
Code:
import pygame,sys,time
from math import *
screen=pygame.display.set_mode((800,600))
G = 5
class Object: #Just an object, like a moon or planet
def __init__(self,mass,init_cds,init_vel,orbit_obj='Sun'):
self.mass = mass
self.cds = init_cds
self.velocity = init_vel
self.accel = [0,0]
self.angle = 0
self.orb_obj = orbit_obj
def display(self):
int_cds = (round(self.cds[0]),round(self.cds[1]))#Stores its co-ordinates as floats, has to convert to integers for draw function
pygame.draw.circle(screen,(255,0,0),int_cds,10)
def calc_gravity(self):
if self.orb_obj == 'Sun':
c_x,c_y = 400,300
c_mass = 10000
else:
c_x,c_y = self.orb_obj.cds
c_mass = self.orb_obj.mass
d_x = self.cds[0]-c_x
d_y = self.cds[1]-c_y
dist = sqrt(d_x**2+d_y**2) #Find direct distance
angle = atan(d_x/d_y) #Find angle
print(d_x,d_y)
print(dist,degrees(angle))
if dist == 0:
acc = 0
else:
acc = G*c_mass/(dist**2) #F=G(Mm)/r^2, a=F/m -> a=GM/r^2
print(acc)
acc_x = acc*sin(angle) #Convert acceleration from magnitude+angle -> x and y components
acc_y = acc*cos(angle)
self.accel = [acc_x,acc_y]
print(self.accel)
self.velocity = [self.velocity[0]+self.accel[0],self.velocity[1]+self.accel[1]] #Add acceleration to velocity
print(self.velocity)
self.cds = (self.cds[0]+self.velocity[0],self.cds[1]+self.velocity[1]) #Change co-ordinates by velocity
print(self.cds)
print('-------------------') #For seperating each run of the function when printing variables
HOM = Object(1000000,(400,100),[10,0]) #The problem planet
clock = pygame.time.Clock()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill((0,0,0))
pygame.draw.circle(screen,(255,255,0),(400,300),25)
HOM.display()
HOM.calc_gravity()
clock.tick(30)
pygame.display.flip()

Your main issue has to do with this line:
angle = atan(d_x/d_y) #Find angle
The atan function is very limited in its ability to compute angles because it can't tell the signs of the coordinates you combined in your division. For instance, it will give the same result for atan(1/1) and atan(-1/-1), since both divisions compute the same slope (1).
Instead you should use atan2, and pass the coordinates separately. Since this will let the code see both coordinates, it can pick an angle in the right quadrant of the circle every time.
But there's an even better fix. Instead of computing an angle and then immediately converting it back to a unit vector (by calling sin and cos on it), why not compute the unit vector directly? You already have the original vector's length! Instead of:
acc_x = acc*sin(angle) #Convert acceleration from magnitude+angle -> x and y components
acc_y = acc*cos(angle)
Use:
acc_x = acc * d_x / distance
acc_y = acc * d_y / distance
The d_x / distance and d_y / distance values are the same as the sin and cos values you were getting before (for the angles when they were working correctly), but there's no need for the trigonometry. You can get rid of the line I quoted up top completely!
Note that you might need to reverse the way you're computing d_x and d_y, so that you get a vector that points from the orbiting object towards the object it's orbiting around (instead of pointing the other way, from the center of the orbit towards the orbiting object). I'm not sure if I'm reading your code correctly, but it looks to me like you have it the other way around right now. That means that you were actually getting the wrong results from atan in the cases where your current code was working the way you expected, and the bad behavior (flying off into nowhere) is the code working "correctly" (from a mathematical point of view). Alternatively, you could compute acc to be negative, rather than positive.
As several commenters mentioned, you may have other issues related to your choice of integration algorithm, but those errors are not going to be as large as the main issue with the acceleration angle. They'll crop up as you run your simulation over longer time periods (and try to use larger time steps to make the simulation go faster). Your current algorithm is good enough for an orbit or two, but if you're simulating dozens or hundreds of orbits, you'll start seeing errors accumulate and so you should pick a better integrator.

Related

Generate a connected line with different amplitude

I'm trying to make a game like Line, but with a horizontal and not vertical wave. The problem is making that the wave continues even after changing its amplitude (I will change the frequency later). So far I have reached this part of wave:
import pygame
import pygame.gfxdraw
import math
import time
DISPLAY_W, DISPLAY_H = 400, 800
clock = pygame.time.Clock()
pygame.init()
SCREEN = pygame.Surface((DISPLAY_W, DISPLAY_H))
GAME_DISPLAY = pygame.display.set_mode((DISPLAY_W, DISPLAY_H))
class Line():
def __init__(self):
self.pointsList = [0]*800
self.listIndex = 0
def game(self):
while True:
clock.tick(60)
SCREEN.fill((0, 0, 0))
self.listIndex += +1
self.generateWave()
self.drawWave()
for event in pygame.event.get():
if (event.type == pygame.QUIT):
quit()
pygame.display.update()
GAME_DISPLAY.blit(SCREEN, (0, 0))
def drawWave(self):
for Y_CORD in range(len(self.pointsList)):
pygame.gfxdraw.pixel(
GAME_DISPLAY, self.pointsList[Y_CORD]-55, DISPLAY_H-Y_CORD, (255, 255, 255))
pygame.gfxdraw.pixel(
GAME_DISPLAY, self.pointsList[Y_CORD]-350, DISPLAY_H-Y_CORD, (255, 255, 255))
def generateWave(self):
waveAmplitude = 50
waveFrequency = 1
XCord = int((DISPLAY_H/2) + waveAmplitude*math.sin(
waveFrequency * ((float(0)/-DISPLAY_W)*(2*math.pi) + (time.time()))))
if self.pointsList[-1] != 0:
self.pointsList.pop(0)
self.pointsList.append(XCord)
else:
self.pointsList[self.listIndex] = XCord
if __name__ == "__main__":
game = Line()
game.game()
I thought about having another function to change the amplitude, but then there would be a gap:
Obligatory reference: https://en.wikipedia.org/wiki/Spline_(mathematics)
Splines have many nice properties, on purpose,
such as matching the slope of inbound and outbound
curve segments. But you're not using splines
in the current code, so let's switch gears.
For current y
you can phrase this in terms of hidden_x
and display_x coordinates.
The trouble with the hidden X is, as you mentioned,
it sadly is discontinuous.
Which would make for an ugly display.
So we won't do that.
But it's a perfectly good ideal to shoot for.
So let's run with that notion.
Initialize display_x to equal hidden_x.
Now as Y advances, the hidden value can jump
around disconcertingly, even discontinuously.
Define some parameter alpha on the unit interval 0 .. 1.
Compute
delta = hidden_x - display_x
For each new Y, advance display_x
in the right direction
by alpha * delta.
Thus, no matter how the hidden X jumps around,
the display X will always smoothly be chasing it.
display_x += alpha * delta
Imagine that alpha was set to 1.0.
Then we're essentially assigning the hidden value
to the display value, right?
For positive values less than 1,
the display value will head in the right direction,
and soon will almost catch up to where
the hidden variable is.
Choosing an appropriate alpha value is up to you.
Largely it's an aesthetic decision,
given that OP did not disclose how errors affect
a cost function.
Play with it, have fun!
For more details on this technique,
look into PID controllers.
Feel free to ignore the integration term.
What we've been looking at are
the Position and Derivative terms.
The PID literature might give you
ideas on how to formalize "goodness of fit"
in your use case.
Non-linear transforms might be applicable,
as well.
assert delta >= 0
display_x += max(max_delta, alpha * delta)
Sometimes we describe this as clipping
the derivative to a maximum slew rate.
(If you choose to adopt this approach,
be sure to use something like sgn()
so that both positive and negative deltas
are handled appropriately.)
Some folks might even prefer to use a quadratic
relation with delta,
in order to catch up to "big" swings more quickly.

Projectile motion: Results show big difference between the line with air resistance and the one without it

I'm simulating the projectile motions with python on Spyder, one with air resistance and one without it. I started off by following this exercise.
It has guided me to do the projectile motion with air resistance. In order to make a comparison, I tried to plot a projectile motion without air resistance in the same graph with the same parameters on my own. However, the resulted graph has come as a surprise for me as it looks a bit off. There's a big difference between this two lines in terms of the range as the other similar graphs I've come across online only show a slight difference.
My graph:
(the smaller one is the one with drag while the bigger one is the one without it)
My questions are :
What caused this? Is it because the drag force I've put only includes drag coefficient and velocity, which made the graph smaller than the one that also has cross sectional area and rho included?
Is the way I'm doing it very complicated and how would you modify it to make it more efficient and neat?
It would be awesome if you're also willing to point out any errors I've made
import numpy as np
import matplotlib.pyplot as plt
# Model parameters
M = 1.0 # Mass of projectile in kg
g = 9.8 # Acceleration due to gravity (m/s^2)
V = 80 # Initial velocity in m/s
ang = 60.0 # Angle of initial velocity in degree
Cd = 0.005 # Drag coefficient
dt = 0.5 # time step in s
# Set up the lists to store variables
# Start by putting the initial velocities at t=0
t = [0] # list to keep track of time
vx = [V*np.cos(ang/180*np.pi)] # list for velocity x and y components
vy = [V*np.sin(ang/180*np.pi)]
# parameters for the projectile motion without drag force
t1=0
vx_nodrag=V*np.cos(ang/180*np.pi)
vy_nodrag=V*np.sin(ang/180*np.pi)
while (t1 < 100):
x_nodrag=vx_nodrag*t1
y_nodrag=vy_nodrag*t1+(0.5*-9.8*t1**2)
plt.ylim([0,500])
plt.xlim([0,570])
plt.scatter(x_nodrag, y_nodrag)
print(x_nodrag,y_nodrag)
t1=t1+dt
# Drag force
drag = Cd*V**2 # drag force
# Create the lists for acceleration components
ax = [-(drag*np.cos(ang/180*np.pi))/M]
ay = [-g-(drag*np.sin(ang/180*np.pi)/M)]
# Use Euler method to update variables
counter = 0
while (counter < 100):
t.append(t[counter]+dt) # increment by dt and add to the list of time
vx.append(vx[counter]+dt*ax[counter])
vy.append(vy[counter]+dt*ay[counter])
# With the new velocity calculate the drag force
vel = np.sqrt(vx[counter+1]**2 + vy[counter+1]**2)
drag = Cd*vel**2
ax.append(-(drag*np.cos(ang/180*np.pi))/M)
ay.append(-g-(drag*np.sin(ang/180*np.pi)/M))
# Increment the counter by 1
counter = counter +1
x=[0]#creating a list for x
y=[0]#creating a list for y
counter1=0
while (counter1<50):
#t.append(t[counter1]+dt),t already has a list.
x.append(x[counter1]+dt*vx[counter1])
y.append(y[counter1]+dt*vy[counter1])
plt.ylim([0,500])
plt.xlim([0,570])
plt.plot(x,y)
#print(x,y)
counter1=1+counter1
# Let's plot the trajectory
plt.plot(x,y,'ro')
plt.ylabel("height")
plt.xlabel("range")
print("Range of projectile is {:3.1f} m".format(x[counter]))
Yes you definitely have to include all parameters of the projectile as the total distance traveled is very sensitive to that.
Generally try to work with functions you can then call in for loops. This makes your code easier to read and more reusable. Implement for example a function which calculates a new time step. Also work with numpy arrays as vectors instead of having vx and vy separately.
Additionally there is an error in your calculations. When calculating the new velocities you also have to compute an updated angle. This is because in every step you have new angle (i.e. in the end of the flight the drag force in y direction has an opposite sign than gravity)
Do for example:
vel = np.sqrt(vx[counter+1]**2 + vy[counter+1]**2)
drag = Cd*vel**2
# calculate new angle for force separation
a, v = np.array([1,0]), np.array([vx[counter+1], vy[counter+1]])
if vy[counter+1] > 0:
ang = np.arccos( np.dot(a,v) / (1*np.linalg.norm(v)) )
else:
ang = 2*np.pi - np.arccos( np.dot(a,v) / (1*np.linalg.norm(v)) )
ax.append(-(drag*np.cos(ang))/M)
ay.append(-g-(drag*np.sin(ang)/M))

How to offset the error in calculating cartesian coordinates from polar coordinates

I'm currently trying to develop a to-scale model of the universe using pygame. At the moment, when I'm calculating the x, y positions of the planets w.r.t. the sun, the planets are slowly falling towards the sun, despite only using equations for position based on the distance and angle of the planet (no force).
Here is the code snippet for calculating distance from a given star currently:
def d_obj(self, reference):
x_diff_sq = pow(self.x - reference.pos[0], 2)
y_diff_sq = pow(self.y - reference.pos[1], 2)
return pow(x_diff_sq + y_diff_sq, 0.5)
And then I pass what this function returns into the next function for calculating the position
def move(self, d):
self.theta += self.d_theta
self.x = int(d * math.cos(self.theta)) + total_d/2
self.y = int(d * math.sin(self.theta)) + total_d/2
total_d/2 is a co-ordinate offset and self.d_theta is the rotational period for the given planet.
Each planet has its initial position hard coded and I'm using this to calculate the difference between initial distance and current distance for all of the planets, every tick it is apparent that the planet moves about 1km towards the sun. Is there any way I can attempt to offset this?
I understand that in the scale of things where I'm drawing things in terms of millions of km, I'm just curious what part of these equations is causing the error. I've tried using the '**' operator over pow and after some research online found that pow is better used for powers involving floats.
Should also mention that all calculations are in kilometers, then before drawing, the planets radius and x, y are mapped to the screen from a set distance that is currently around 4 AU.
You're trying to move your planets in circles, right?
In your code, you
Use x and y to calculate distance,
Use delta_theta to calculate new theta,
Use new theta and distance to calculate new x and y.
You don't have to do all that. Instead, you can keep a hardcoded distance and just
Use delta_theta to calculate new theta,
Use new theta and (known) distance to calculate x and y for drawing.
Then your distance will not drift at all.
Side note: If you're planning to keep the planets moving for long times, make sure you keep your theta between 0 and 2*pi, or rounding errors will start kicking in and your theta accuracy will deteriorate.
You're thinking this will make adding moons and asteroids difficult.
Not really!
You can do the same for moons, by noting which planet they belong to, the distance to that planet, delta_theta and initial theta (based on their parent planet).
If you want to start doing ellipses instead of circles, you can change your calculations (use convenient constant orbital elements instead of distance and delta_theta, which will not be constant anymore) to apply Kepler's laws.
You can also add asteroids later. You can keep the Cartesian positions and velocities of the asteroids, and calculate their motion separately, after calculating the motion of all the "trivially" moving objects.

moving a rectangle to a specific point

I am trying to move a rectangle to a specific point with a specific speed.
however it only works properly if x,y (the point i am trying to move it to) are the same. otherwise, if x was bigger than y it would move at a 45 degree angle until self.location[1]==y(it reached where it needed to be for y), then it would move in a straight for x and vise versa.
i know that i would have to change speed_y so that it was slower. how do i work out what speed i need y to be at in order to get the rectangle to move to location in a straight line no matter what location is?
full function:
def move(self,x,y,speed):
if speed==0:
self.location=[x,y]
else:
speed_x = speed
speed_y = speed
if x > 0 and not self.location[0]==x: # x is positive and not already where it needs to be
if not x == y:
speed_x = something # need to slow down the speed so that it gets to y as the same time as it gets to x
self.speed_x=speed_x
else: self.speed_x=0
if y > 0 and not self.location[1]==y: # same for y
if not x == y:
speed_y = something # need to slow down the speed so that it gets to y as the same time as it gets to x
self.speed_y=speed_y
else: self.speed_y=0
You should set your speeds to a ratio of required distance for each axis.
e.g. if distance to x is half distance to y then speed_x should be half speed_y.
Further example as requested:
distance_x = x-self.location[0]
distance_y = y-self.location[1]
if abs(distance_x) < abs(distance_y):
ratio = distance_x/abs(distance_y)
speed_x = ratio * speed
edit: Reworked the directions of example.
I don't quite get what you are asking for, but see if this helps you:
speed_x = speed*(cos(atan2((y-self.location[1]), (x-self.location[0]))))
speed_y = speed*(sin(atan2((y-self.location[1]), (x-self.location[0]))))
That would "slow" the speed you are given by splitting it in the values needed to get to where you want your box to be at the same time.
Pardon any errors in my english/python, im not native on either one :)
You need mathematical calculations to get new position in every move.

How to measure distance at angle in image python

I'm working on a particle filter for an autonomous robot right now, and am having trouble producing expected distance measurements by which to filter the particles. I have an image that I'm using as a map. Each pixel represents a certain scaled area in the enviroment. Space the robot can occupy is white, walls are black, and areas that are exterior to the enviroment are grey.
If you are unfamiliar with what a particle filter is, my python code will create a predetermined number of random guesses as to where it might be (x,y,theta) in the white space. It will then measure the distance to the nearest wall with ultrasonic sensors at several angles. The script will compare these measurements with the measurements that would have been expected at each angle for each guessed location/orientation. Those that most closely match the actual measurements will survive while guesses that are less likely to be right will be eliminated.
My problem is finding the nearest wall AT a given angle. Say the sensor is measuring at 60°. For each guess, I need to adjust the angle to account for the guessed robot orientation, and then measure the distance to the wall at that angle. It's easy enough find the nearest wall in the x direction:
from PIL import Image
#from matplotlib._png import read_png
from matplotlib.pyplot import *
mapp = Image.open("Map.png")
pixels = mapp.load()
width = mapp.size[0]
height = mapp.size[1]
imshow(mapp)
pixelWidth = 5
for x in range(width):
if mapp.getpixel((x, 100)) == (0,0,0,255): #Identify the first black pixel
distance = x*pixelWidth self.x
The problem is that I can't tell the script to search one pixel at a time going at a 60°, or 23°, or whatever angle. Right now the best thing I can think of is to go in the x direction first, find a black pixel, and then use the tangent of the angle to determine how many pixels I need to move up or down, but there are obvious problems with this, mostly having to do with corners, and I can't imagine how many if statements it's going to take to work around it. Is there another solution?
Okay, I think I found a good approximation of what I'm trying to do, though I'd still like to hear if anyone else has a better solution. By checking the tangent of the angle I've actually traveled so far between each pixel move, I can decide whether to move one pixel in the x-direction, or in the y-direction.
for i in range(len(angles)):
angle = self.orientation+angles[i]
if angle > 360:
angle -= 360
x = self.x
y = self.y
x1 = x
y1 = y
xtoy_ratio = tan(angle*math.pi/180)
if angle < 90:
xadd = 1
yadd = 1
elif 90 < angle < 180:
xadd = -1
yadd = 1
elif 180 < angle < 270:
xadd = -1
yadd = -1
else:
xadd = 1
yadd = -1
while mapp.getpixel(x,y) != (0,0,0,255):
if (y-y1)/(x-x1) < xtoy_ratio:
y += yadd
else:
x += xadd
distance = sqrt((y-y1)^2+(x-x1)^2)*pixel_width
The accuracy of this method of course depends a great deal on the actual length represented by each pixel. As long as pixel_width is small, accuracy will be pretty good, but if not, it will generally go pretty far before correcting itself.
As I said, I welcome other answers.
Thanks

Categories

Resources