I've recently been tasked with writing a Python based app that generates an STL file (a stereolithography file format - basically a list of triangles). I'm new to python, and I'm trying to avoid using overly complicated libraries. The issue that I'm having is with 3D rotations in a vector class that I've created.
My rotation function in the vector class looks like the following:
def rotateY(self, amount):
'''Rotates a vertex on the Y axis around the origin by a given amount (in radians)'''
self.x = self.x * math.cos(amount) + self.z * math.sin(amount)
self.y = self.y
self.z = self.x * (-math.sin(amount)) + self.z * math.cos(amount)
As you may have guessed, the vector class has an x, y, and z component. I'm reasonably familiar with 3D transformations and math, and the code looks correct, as far as I can tell. I'd rather not have to pull in some other matrix library for the sake of keeping things simple.
The problem arises when I attempt to do something like this:
v = vector(50.0, 0.0, 0.0)
rotationAmount = math.radians(10.0)
for i in range(0, 36):
v.rotateY(rotationAmount)
#Draw something at (v.x, v.y, v.z),
#...or in my case, create a list of triangles to export to an STL file
As I rotate around the y axis, the distance from the origin to the point being rotated slowly decreases. Here's an example, where I'm creating a 3D sphere at the location of the vector after each rotation, and exporting it all as an STL (as show in Maya):
As you can see, the distance from the origin to the vector being rotated is definitely decreasing the more times the vector is rotated. This is also backed up by printing out the length of the vector after each rotation, as shown (the initial vector is set to a position of (50.0, 0.0, 0.0):
Just as s temporary hack, I tried finding the length of the vector, normalizing it, rotating it, normalizing it again, then scaling back by the original size, and this seemed to have worked, but it seems like a very dirty hack to get something as simple as this to work.
My question is: what is causing the length of the vector to slowly decrease?
EDIT: I should have been a little more clear. The vector class only supports rotations around the origin.
There is a problem in your rotateY function. You are modifying x then using that modified x to calculate your modified z. This is causing your spiraling doom. One way to fix it would be to store your x in a temp variable so you can calculate your modified z off the original x. This will maintain your magnitude.
def rotateY(self, amount):
'''Rotates a vertex on the Y axis around the origin by a given amount (in radians)'''
temp_x = self.x
self.x = self.x * math.cos(amount) + self.z * math.sin(amount)
self.z = temp_x * (-math.sin(amount)) + self.z * math.cos(amount)
Related
I have a Support Vector Machine that splits my data in two using a decision hyperplane (for visualisation purposes this is a sample dataset with three dimensions), like this:
Now I want to perform a change of basis, such that the hyperplane lies flatly on the x/y plane, such that the distance from each sample point to the decision hyperplane is simply their z-coordinate.
For that, I know that I need to perform a change of basis. The hyperplane of the SVM is given by their coefficient (3d-vector) and intercept (scalar), using (as far as I understand it) the general form for mathematical planes: ax+by+cz=d, with a,b,c being the coordinates of the coefficient and d being the intercept. When plotted as 3d-Vector, the coefficient is a vector orthogonal to the plane (in the image it's the cyan line).
Now to the change of basis: If there was no intercept, I could just assume the vector that is the coefficient is one vector of my new basis, one other can be a random vector that is on the plane and the third one is simply cross product of both, resulting in three orthogonal vectors that can be the column vectors of the transformation-matrix.
The z-function used in the code below comes from simple term rearrangement from the general form of planes: ax+by+cz=d <=> z=(d-ax-by)/c:
z_func = lambda interc, coef, x, y: (interc-coef[0]*x -coef[1]*y) / coef[2]
def generate_trafo_matrices(coefficient, z_func):
normalize = lambda vec: vec/np.linalg.norm(vec)
uvec2 = normalize(np.array([1, 0, z_func(1, 0)]))
uvec3 = normalize(np.cross(uvec1, uvec2))
back_trafo_matrix = np.array([uvec2, uvec3, coefficient]).T
#in other order such that its on the xy-plane instead of the yz-plane
trafo_matrix = np.linalg.inv(back_trafo_matrix)
return trafo_matrix, back_trafo_matrix
This transformation matrix would then be applied to all points, like this:
def _transform(self, points, inverse=False):
trafo_mat = self.inverse_trafo_mat if inverse else self.trafo_mat
points = np.array([trafo_mat.dot(point) for point in points])
return points
Now if the intercept would be zero, that would work perfectly and the plane would be flat on the xy-axis. However as soon as I have an intercept != zero, the plane is not flat anymore:
I understand that that is the case because this is not a simple change of basis, because the coordinate origin of my other basis is not at (0,0,0) but at a different place (the hyperplane could be crossing the coefficient-vector at any point), but my attempts of adding the intercept to the transformation all didn't lead to the correct result:
def _transform(self, points, inverse=False):
trafo_mat = self.inverse_trafo_mat if inverse else self.trafo_mat
intercept = self.intercept if inverse else -self.intercept
ursprung_translate = trafo_mat.dot(np.array([0,0,0])+trafo_matrix[:,0]*intercept)
points = np.array([point+trafo_matrix[:,0]*intercept for point in points])
points = np.array([trafo_mat.dot(point) for point in points])
points = np.array([point-ursprung_translate for point in points])
return points
is for example wrong. I am asking this on StackOverflow and not on the math StackExchange because I think I wouldn't be able to translate the respective math into code, I am glad I even got this far.
I have created a github gist with the code to do the transformation and create the plots at https://gist.github.com/cstenkamp/0fce4d662beb9e07f0878744c7214995, which can be launched using Binder under the link https://mybinder.org/v2/gist/jtpio/0fce4d662beb9e07f0878744c7214995/master?urlpath=lab%2Ftree%2Fchange_of_basis_with_translate.ipynb if somebody wants to play around with the code itself.
Any help is appreciated!
The problem here is that your plane is an affine space, not a vector space, so you can't use the usual transform matrix formula.
A coordinate system in affine space is given by an origin point and a basis (put together, they're called an affine frame). For example, if your origin is called O, the coordinates of the point M in the affine frame will be the cooordinates of the OM vector in the affine frame's basis.
As you can see, the "normal" R^3 space is a special case of affine space where the origin is (0,0,0).
Once we've determined those, we can use the frame change formulas in affine spaces: if we have two affine frames R = (O, b) and R' = (O', b'), the base change formula for a point M is: M(R') = base_change_matrix_from_b'_to_b * (M(R) - O'(R)) (with O'(R) the coordinates of O' in the coordinate system defined by R).
In our case, we're trying to go from the frame with an origin at (0,0,0) and
the canonical basis, to a frame where the origin is the orthogonal projection of (0,0,0) on the plane and the basis is, for instance, the one described in your initial post.
Let's implement these steps:
To begin with, we'll define a Plane class to make our lifes a bit easier:
from dataclasses import dataclass
import numpy as np
#dataclass
class Plane:
a: float
b: float
c: float
d: float
#property
def normal(self):
return np.array([self.a, self.b, self.c])
def __contains__(self, point:np.array):
return np.isclose(self.a*point[0] + self.b*point[1] + self.c*point[2] + self.d, 0)
def project(self, point):
x,y,z = point
k = (self.a*x + self.b*y + self.c*z + self.d)/(self.a**2 + self.b**2 + self.c**2)
return np.array([x - k*self.a, y-k*self.b, z-k*self.c])
def z(self, x, y):
return (- self.d - self.b*y - self.a*x)/self.c
We can then implement make_base_changer, which takes a Plane as an input, and return 2 lambda functions performing the forward and inverse transform (taking and returning a point). You should be able to test
def normalize(vec):
return vec/np.linalg.norm(vec)
def make_base_changer(plane):
uvec1 = plane.normal
uvec2 = [0, -plane.d/plane.b, plane.d/plane.c]
uvec3 = np.cross(uvec1, uvec2)
transition_matrix = np.linalg.inv(np.array([uvec1, uvec2, uvec3]).T)
origin = np.array([0,0,0])
new_origin = plane.project(origin)
forward = lambda point: transition_matrix.dot(point - new_origin)
backward = lambda point: np.linalg.inv(transition_matrix).dot(point) + new_origin
return forward, backward
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.
I am trying to perform a simple task using simple math in python and I suspect that the inherit error in converting from radians to degrees as a result of an error with floating point math (as garnered from another question on the topic please don't mark this as a duplicate question, it's not).
I am trying to extend a line by 500m. To do this I am taking the the endpoint coordinates from a supplied line and using the existing heading of said line to generate the coordinates of the point which is 500m in the same heading.
Heading is important in this case as it is the source of my error. Or so I suspect.
I use the following function to calculate the interior angle of my right angle triangle, built using the existing line, or in this case my hypotenuse:
def intangle(xypoints):
angle = []
for i in xypoints:
x1 = i[0][0]
x2 = i[1][0]
y1 = i[0][1]
y2 = i[1][1]
gradient = (x1 - x2)/(y1-y2)
radangle = math.atan(gradient)
angle.append((math.degrees(radangle)))
return angle
My input points are, for example:
(22732.23679147904, 6284399.7935522054)
(20848.591367954294, 6281677.926560438)
I know going into this that my angle is 35° as these coordinates are programmatically generated by a separate function and when plotted are out by around 3.75" for each KM. Another error as a result of converting radians to degrees but acceptable in its scope.
The error generated by the above function however, results in an angle that plots my new endpoint in such a place that the line is no longer perfectly straight when I connect the dots and I absolutely have to have a straight line.
How can I go about doing this differently to account for the floating point error? Is it even possible? If not, then what would be an acceptable method of extending my line by howevermany meters using euclidean geometry?
To add to this, I have already done all relevant geographic conversions and I am 100% sure that I am working on a 2D plane so the ellipsoid and such do not play a role in this at all.
Using angles is unnecessary, and there are problems in the way you do it. Using the atan will only give you angles between -pi/2 and pi/2, and you will get the same angle value for opposite directions.
You should rather use Thales:
import math
a = (22732.23679147904, 6284399.7935522054)
b = (20848.591367954294, 6281677.926560438)
def extend_line(a, b, length):
"""
Returns the coordinates of point C at length beyond B in the direction of A->B"""
ab = math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2)
coeff = (ab + length)/ab
return (a[0] + coeff*(b[0]-a[0]), a[1] + coeff*(b[1]-a[1]) )
print(extend_line(a, b, 500))
# (20564.06031560228, 6281266.7792872535)
I wrote a program that computes and animates the orbit of pluto, and have begun rewriting it using classes because this seems like a sensible way of introducing more planets into the simulation. i.e have a class that defines the physics, and then feed in specific planet data to get the orbital data.
class Planet(object):
m_sun = 1.989*(10**30)
G = 6.67*(10**-11)
dt = 1
coords = []
def __init__(self, x, y, vx, vy, m):
self.x = x
self.y = y
self.vx = vx
self.vy = vy
self.m = m
def genData(self):
while self.dt < 100000000:
r = ((self.x)**2 + (self.y)**2)**0.5
a = ((self.G*self.m_sun)/r**2)
ax = -a*((self.x)/r)
ay = -a*((self.y)/r)
self.vx = self.vx + ax*self.dt
self.vy = self.vy + ay*self.dt
self.x = self.x + self.vx*self.dt
self.y = self.y + self.vy*self.dt
coord = (self.x, self.y)
print coord
self.coords.append(coord)
self.dt = self.dt + 1000
pluto = Planet(4495978707000, 0, 0, 4670, 1.305*(10**22))
pluto.genData()
I'm sure it isn't perfect, but it appears to be working (this is the first class i've built on my own). My question is how do I extract the data from 'coords' into a list that I can work with outside of the class.
I want to generate data for each planet, and then use this data to create an animation in Pygame. For example, a list of (x,y) coordinates for pluto, earth, saturn etc. As it stands, it churns out the data, but it doesn't appear to be accessible from outside the class.
I hope my question makes sense.
Thanks!
Your question's been answered, but I have some info you should find useful to improve your program. Technically, this info belongs in a comment (since you didn't actually ask about this in your question), but it wouldn't fit. :)
Pluto's orbit is inclined quite a bit to the ecliptic, so if you want to plot it along with several other planets in the Solar System you need to work in 3D to be accurate. But I guess that's not a big deal for your application.
Earth's orbit is tiny compared to Pluto, so you'll probably need to implement zooming to see them both on one animation.
You can get more accuracy in your calculations by using the Standard gravitational parameter rather than using separate values of G and mass.
Your algorithm for calculating velocity and position is called Euler integration. It's equivalent to approximating the curve of the orbit by a polygon. It works, but it's not very accurate. So you need to make the time delta very small otherwise the orbit won't be very realistic and it may not even be a closed curve. And even that doesn't help a lot because the error is accumulative, so eventually the computed orbit will bear little resemblance to reality.
No technique of numerical integration is perfect (except on very simple functions), but a popular family of integration techniques that are more accurate (and hence work ok with a much larger time step) are the Runge-Kutta methods. You can find lots of example code of orbit calculation using a Runge-Kutta method; most code examples use the variant known as RK4.
However, I strongly urge you to try Leapfrog integration. It's quite easy to code the synchronized form of Leapfrog and it has a major benefit over Runge-Kutta in that it's symplectic, which (roughly) means that it conserves energy, so the error won't accumulate from orbit to orbit.
Did you try pluto.coords?
You can access members of a class from outside by using the instance followed by dot followed by the member name, i.e. attribute access. This is just as you have done when calling the genData() method.
BTW, you can define your constants using exponential notation:
m_sun = 1.989e+30
G = 6.67e-11
and
pluto = Planet(4495978707000, 0, 0, 4670, 1.305e+22)
which is more readable (important) and saves a few calculations for the definition of your class (less/not important).
Instead of storing the values in self.coords, yield the values:
def genData(self):
while self.dt < 100000000:
...
self.x = self.x + self.vx*self.dt
self.y = self.y + self.vy*self.dt
coord = (self.x, self.y)
yield coord
self.dt = self.dt + 1000
pluto = Planet(4495978707000, 0, 0, 4670, 1.305*(10**22))
for coord in pluto.genData():
print(coord)
Note that this makes genData a generator function.
To obtain a list of coords, you can accumulate the values in the loop:
coords = []
for coord in pluto.genData():
coords.append(coord)
or use coords = list(pluto.genData()).
By the way, it's usually a good policy to separate code-that-calculates from code-that-prints. That way you can call code-that-calculates many times, or include it in a chain of calculations without always emitting print statements.
I want to generate data for each planet, and then use this data to create an animation in Pygame.
It sounds like you don't need to accumulate the data. You can plot the current point for each planet without needing to know the planet's coordinate history. In that case, using generator functions would be more memory-efficient since they would allow you to generate the next coordinate for each planet without having to store all the coordinates in a list first:
# In Python2
import itertools as IT
for coords in IT.izip(*[planet.genData() for planet in [pluto, neptune, uranus, ...]]):
# plot coords
or
# In Python3
for coords in zip(*[planet.genData() for planet in [pluto, neptune, uranus, ...]]):
# plot coords
Actually it's very easy, just pluto.coords:
pluto = Planet(4495978707000, 0, 0, 4670, 1.305*(10**22))
pluto.genData()
print pluto.coords
NOTE: Because I am a new member and I apparently need 10 reputation to post images or more than 2 links, I will be referring to this imgur album for visuals. Sorry for any inconvenience.
I'm trying to make a program to visualize 3D objects in python, but I'm having a problem with the function that projects a 3D coordinate onto 2D screen coordinates. It works in some cases, but not all. When the camera is relatively far away from the point and about on the same level, the projection looks very nice, such as in this image depicting a decagon and rectangle, whose vertices are defined by 3D points (Figure 2). However, when the camera is looking down on the points, the object they depict is flattened disproportionately (Figure 3), and when the camera is close to the points it must project, the shape gets warped (Figure 4).
The function in question is Camera.projectPoint, and I have included all of the functions and classes that it uses in the following:
class Vector3:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
self.mag = math.sqrt(x**2+y**2+z**2)
self.yaw = math.atan2(self.y, self.x)
self.pitch = math.atan2(self.z, math.sqrt(self.x**2+self.y**2))
class Point:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def vectorTo(self, p):
return(Vector3(p.x-self.x, p.y-self.y, p.z-self.z))
class Camera:
def __init__(self, pos, yaw, pitch, FOV=math.pi/2):
self.pos = pos
self.yaw = yaw
self.pitch = pitch
self.FOV = FOV
def projectPoint(self, point):
## finding the vector from the camera position to the point
v = self.pos.vectorTo(point)
## setting alpha to the difference in yaw between the camera and the vector to the point
## and beta to the difference in pitch
alpha = v.yaw - self.yaw
beta = v.pitch - self.pitch
## making sure that the smallest angle between the two is chosen
## (difference between 300 degrees and 10 degrees is 70, not 290)
alpha = (alpha+math.pi)%(2*math.pi)-math.pi
beta = (beta+math.pi)%(2*math.pi)-math.pi
## Doing the operation pictured in the diagram
h = math.sin(self.FOV/2)/math.cos(self.FOV/2)
x1 = math.sin(alpha)/math.cos(alpha)
y1 = math.sin(beta)/math.cos(beta)
## adjusting for screen resolution
return(x1*1000/h+500, y1*1000/h+325)
I have looked around for algorithms to project a 3D coordinate onto the screen, and have found a lot of things such as what is depicted in Figure 1 (which is what my function is based off of), but it doesn't seem to be working very well. Is it a problem with the algorithm? The way I implimented it? One of the functions it uses? (I'm like 99% sure all of the other functions and classes are perfectly fine). Any ideas on what's wrong and how to fix it? Thank you.