I'm working on straightening splines as a component of my larger project to straighten curved text.
After fitting a spline to my data points, I use scipy's splev to get the derivative of the spline at each point along the curve. Since the derivative gives me the slope of the tangent to the curve at a given point (unless I'm very confused), I determine the rotation needed to produce a straight line by comparing the derivative to a line with 0 slope.
Having established the rotation needed at each point to straighten my spline, I loop over each point and apply the corrective rotation to the current point and each preceding point.
The relevant code follows:
import numpy as np
from numpy import arange
from scipy import interpolate
import matplotlib.pyplot as plt
import math
import random
def rotate(origin, point, angle):
ox, oy = origin
px, py = point
qx = ox + math.cos(angle) * (px - ox) - math.sin(angle) * (py - oy)
qy = oy + math.sin(angle) * (px - ox) + math.cos(angle) * (py - oy)
return qx, qy
xxx = [0,2,4,4,2,0]
yyy = [0,2,4,6,8,10]
tckp, u = interpolate.splprep([xxx, yyy], s=3, k=2, nest=-1)
xpointsnew, ypointsnew = interpolate.splev(u, tckp)
dx, dy = interpolate.splev(u, tckp, der=1)
fullder = dy/dx
rotating_x = xxx
rotating_y = yyy
index = -1
for i in fullder:
index += 1
corrective_rotation = -(math.degrees(math.atan(0)-math.atan(fullder[index])))
print(corrective_rotation)
rotation_center = [rotating_x[index], rotating_y[index]]
target_indices = np.arange(0,index,1)
for i in target_indices:
rotation_target = [rotating_x[i], rotating_y[i]]
qx, qy = rotate(rotation_target,rotation_center,math.radians(corrective_rotation))
rotating_x[i] = qx
rotating_y[i] = qy
print(rotating_x)
print(rotating_y)
plt.plot(xpointsnew, ypointsnew, 'r-')
plt.plot(rotating_x, rotating_y, 'b-')
plt.show()
What I'm doing isn't working, but I'm not sure why. Not only is the resulting line not straight, it's also much shorter than the original curve. Is the approach outlined above fundamentally flawed in some way? Am I doing something stupid in my code? I would really appreciate a second pair of eyes.
A fundamental flaw of the algorithm is that it takes slope at a point as the amount of necessary rotation of one of two segments in which that point divides the curve. As an example, consider a straight line at 60 degrees. Your algorithm will produce rotation of 60 degrees at every knot of the line, in effect making them all 120-degree angles.
You are not rotating the entire curve, only a part of it (up to index in your version; after i in my version). The appropriate amount of rotation is how sharply the curve turns at that point, which is reflected by the change of its slope — not the slope itself.
Then there are minor details like
incorrect order of rotation_center and rotation_target in the list of arguments;
pointless conversion to degrees and back;
using atan(dy/dx) where atan2(dy, dx) should be used;
and the strange decision to rotate from the end of the curve.
Here is my version; the only changes are in the for loop.
for i in range(len(xxx)-1):
corrective_rotation = -(math.atan2(dy[i+1], dx[i+1]) - math.atan2(dy[i], dx[i]))
print(corrective_rotation)
rotation_center = [rotating_x[i], rotating_y[i]]
for k in range(i+1, len(xxx)):
rotation_target = [rotating_x[k], rotating_y[k]]
qx, qy = rotate(rotation_center, rotation_target, corrective_rotation)
rotating_x[k] = qx
rotating_y[k] = qy
By the way, plt.axes().set_aspect('equal') helps avoid the illusion that the curve changed length after rotation.
Finally, I should say that taking angles from the point values of the derivative of an interpolating spline is a very questionable decision. Finite differences at an appropriate scale are more robust.
Related
I'm trying to use ray casting to gather all the surfaces in a room and determine it's volume.
I have a centroid location where the rays will be coming from, but I'm drawing a blank on how to get the rays in all 360 degrees (in 3D space).
I'm not getting any points on the floors or ceilings, it's like it's doing a 60 degree spread rotated about the Z axis.
I think I have the rest of it working, but this is stumping me.
for y in range(360):
for x in range(360):
vector = DB.XYZ(math.sin(math.radians(x)), math.cos(math.radians(x)), math.cos(math.radians(y))).Normalize()
prox = ri.FindNearest(origin, direction).Proximity
point = origin + (direction * prox)
Look at it this way: x and y of vector are created from angle x (-> a circle in the plane) and then you add a z component which lies between -1 and 1 (which cos does). So it's obvious that you end up with a cylindrical distribution.
What you might want are spherical coordinates. Modify your code like this:
for y in range(-90, 91):
for x in range(360):
vector = DB.XYZ(math.sin(math.radians(x)) * cos(math.radians(y)),
math.cos(math.radians(x)) * cos(math.radians(y)),
math.sin(math.radians(y))) # Normalize unnecessary, since vector² = sin² * cos² + cos² * cos² + sin² = 1
prox = ri.FindNearest(origin, direction).Proximity
point = origin + (direction * prox)
But be aware that the angle distribution of rays is not uniform using spherical coordinates. At the poles it's more dense than at the equator. You can mitigate this e.g. by scaling the density of x down, depending on y. The surface elements scale down by cos(y)², so I think you have to scale by cos(y).
I am trying to sample around 1000 points from a 3-D ellipsoid, uniformly. Is there some way to code it such that we can get points starting from the equation of the ellipsoid?
I want points on the surface of the ellipsoid.
Theory
Using this excellent answer to the MSE question How to generate points uniformly distributed on the surface of an ellipsoid? we can
generate a point uniformly on the sphere, apply the mapping f :
(x,y,z) -> (x'=ax,y'=by,z'=cz) and then correct the distortion
created by the map by discarding the point randomly with some
probability p(x,y,z).
Assuming that the 3 axes of the ellipsoid are named such that
0 < a < b < c
We discard a generated point with
p(x,y,z) = 1 - mu(x,y,y)/mu_max
probability, ie we keep it with mu(x,y,y)/mu_max probability where
mu(x,y,z) = ((acy)^2 + (abz)^2 + (bcx)^2)^0.5
and
mu_max = bc
Implementation
import numpy as np
np.random.seed(42)
# Function to generate a random point on a uniform sphere
# (relying on https://stackoverflow.com/a/33977530/8565438)
def randompoint(ndim=3):
vec = np.random.randn(ndim,1)
vec /= np.linalg.norm(vec, axis=0)
return vec
# Give the length of each axis (example values):
a, b, c = 1, 2, 4
# Function to scale up generated points using the function `f` mentioned above:
f = lambda x,y,z : np.multiply(np.array([a,b,c]),np.array([x,y,z]))
# Keep the point with probability `mu(x,y,z)/mu_max`, ie
def keep(x, y, z, a=a, b=b, c=c):
mu_xyz = ((a * c * y) ** 2 + (a * b * z) ** 2 + (b * c * x) ** 2) ** 0.5
return mu_xyz / (b * c) > np.random.uniform(low=0.0, high=1.0)
# Generate points until we have, let's say, 1000 points:
n = 1000
points = []
while len(points) < n:
[x], [y], [z] = randompoint()
if keep(x, y, z):
points.append(f(x, y, z))
Checks
Check if all points generated satisfy the ellipsoid condition (ie that x^2/a^2 + y^2/b^2 + z^2/c^2 = 1):
for p in points:
pscaled = np.multiply(p,np.array([1/a,1/b,1/c]))
assert np.allclose(np.sum(np.dot(pscaled,pscaled)),1)
Runs without raising any errors. Visualize the points:
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(projection="3d")
points = np.array(points)
ax.scatter(points[:, 0], points[:, 1], points[:, 2])
# set aspect ratio for the axes using https://stackoverflow.com/a/64453375/8565438
ax.set_box_aspect((np.ptp(points[:, 0]), np.ptp(points[:, 1]), np.ptp(points[:, 2])))
plt.show()
These points seem evenly distributed.
Problem with currently accepted answer
Generating a point on a sphere and then just reprojecting it without any further corrections to an ellipse will result in a distorted distribution. This is essentially the same as setting this posts's p(x,y,z) to 0. Imagine an ellipsoid where one axis is orders of magnitude bigger than another. This way, it is easy to see, that naive reprojection is not going to work.
Consider using Monte-Carlo simulation: generate a random 3D point; check if the point is inside the ellipsoid; if it is, keep it. Repeat until you get 1,000 points.
P.S. Since the OP changed their question, this answer is no longer valid.
J.F. Williamson, "Random selection of points distributed on curved surfaces", Physics in Medicine & Biology 32(10), 1987, describes a general method of choosing a uniformly random point on a parametric surface. It is an acceptance/rejection method that accepts or rejects each candidate point depending on its stretch factor (norm-of-gradient). To use this method for a parametric surface, several things have to be known about the surface, namely—
x(u, v), y(u, v) and z(u, v), which are functions that generate 3-dimensional coordinates from two dimensional coordinates u and v,
The ranges of u and v,
g(point), the norm of the gradient ("stretch factor") at each point on the surface, and
gmax, the maximum value of g for the entire surface.
The algorithm is then:
Generate a point on the surface, xyz.
If g(xyz) >= RNDU01()*gmax, where RNDU01() is a uniform random variate in [0, 1), accept the point. Otherwise, repeat this process.
Chen and Glotzer (2007) apply the method to the surface of a prolate spheroid (one form of ellipsoid) in "Simulation studies of a phenomenological model for elongated virus capsid formation", Physical Review E 75(5), 051504 (preprint).
Here is a generic function to pick a random point on a surface of a sphere, spheroid or any triaxial ellipsoid with a, b and c parameters. Note that generating angles directly will not provide uniform distribution and will cause excessive population of points along z direction. Instead, phi is obtained as an inverse of randomly generated cos(phi).
import numpy as np
def random_point_ellipsoid(a,b,c):
u = np.random.rand()
v = np.random.rand()
theta = u * 2.0 * np.pi
phi = np.arccos(2.0 * v - 1.0)
sinTheta = np.sin(theta);
cosTheta = np.cos(theta);
sinPhi = np.sin(phi);
cosPhi = np.cos(phi);
rx = a * sinPhi * cosTheta;
ry = b * sinPhi * sinTheta;
rz = c * cosPhi;
return rx, ry, rz
This function is adopted from this post: https://karthikkaranth.me/blog/generating-random-points-in-a-sphere/
One way of doing this whch generalises for any shape or surface is to convert the surface to a voxel representation at arbitrarily high resolution (the higher the resolution the better but also the slower). Then you can easily select the voxels randomly however you want, and then you can select a point on the surface within the voxel using the parametric equation. The voxel selection should be completely unbiased, and the selection of the point within the voxel will suffer the same biases that come from using the parametric equation but if there are enough voxels then the size of these biases will be very small.
You need a high quality cube intersection code but with something like an elipsoid that can optimised quite easily. I'd suggest stepping through the bounding box subdivided into voxels. A quick distance check will eliminate most cubes and you can do a proper intersection check for the ones where an intersection is possible. For the point within the cube I'd be tempted to do something simple like a random XYZ distance from the centre and then cast a ray from the centre of the elipsoid and the selected point is where the ray intersects the surface. As I said above, it will be biased but with small voxels, the bias will probably be small enough.
There are libraries that do convex shape intersection very efficiently and cube/elipsoid will be one of the options. They will be highly optimised but I think the distance culling would probably be worth doing by hand whatever. And you will need a library that differentiates between a surface intersection and one object being totally inside the other.
And if you know your elipsoid is aligned to an axis then you can do the voxel/edge intersection very easily as a stack of 2D square intersection elipse problems with the set of squares to be tested defined as those that are adjacent to those in the layer above. That might be quicker.
One of the things that makes this approach more managable is that you do not need to write all the code for edge cases (it is a lot of work to get around issues with floating point inaccuracies that can lead to missing or doubled voxels at the intersection). That's because these will be very rare so they won't affect your sampling.
It might even be quicker to simply find all the voxels inside the elipse and then throw away all the voxels with 6 neighbours... Lots of options. It all depends how important performance is. This will be much slower than the opther suggestions but if you want ~1000 points then ~100,000 voxels feels about the minimum for the surface, so you probably need ~1,000,000 voxels in your bounding box. However even testing 1,000,000 intersections is pretty fast on modern computers.
Depending on what "uniformly" refers to, different methods are applicable. In any case, we can use the parametric equations using spherical coordinates (from Wikipedia):
where s = 1 refers to the ellipsoid given by the semi-axes a > b > c. From these equations we can derive the relevant volume/area element and generate points such that their probability of being generated is proportional to that volume/area element. This will provide constant volume/area density across the surface of the ellipsoid.
1. Constant volume density
This method generates points on the surface of an ellipsoid such that their volume density across the surface of the ellipsoid is constant. A consequence of this is that the one-dimensional projections (i.e. the x, y, z coordinates) are uniformly distributed; for details see the plot below.
The volume element for a triaxial ellipsoid is given by (see here):
and is thus proportional to sin(theta) (for 0 <= theta <= pi). We can use this as the basis for a probability distribution that indicates "how many" points should be generated for a given value of theta: where the area density is low/high, the probability for generating a corresponding value of theta should be low/high, too.
Hence, we can use the function f(theta) = sin(theta)/2 as our probability distribution on the interval [0, pi]. The corresponding cumulative distribution function is F(theta) = (1 - cos(theta))/2. Now we can use Inverse transform sampling to generate values of theta according to f(theta) from a uniform random distribution. The values of phi can be obtained directly from a uniform distribution on [0, 2*pi].
Example code:
import matplotlib.pyplot as plt
import numpy as np
from numpy import sin, cos, pi
rng = np.random.default_rng(seed=0)
a, b, c = 10, 3, 1
N = 5000
phi = rng.uniform(0, 2*pi, size=N)
theta = np.arccos(1 - 2*rng.random(size=N))
x = a*sin(theta)*cos(phi)
y = b*sin(theta)*sin(phi)
z = c*cos(theta)
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.scatter(x, y, z, s=2)
plt.show()
which produces the following plot:
The following plot shows the one-dimensional projections (i.e. density plots of x, y, z):
import seaborn as sns
sns.kdeplot(data=dict(x=x, y=y, z=z))
plt.show()
2. Constant area density
This method generates points on the surface of an ellipsoid such that their area density is constant across the surface of the ellipsoid.
Again, we start by calculating the corresponding area element. For simplicity we can use SymPy:
from sympy import cos, sin, symbols, Matrix
a, b, c, t, p = symbols('a b c t p')
x = a*sin(t)*cos(p)
y = b*sin(t)*sin(p)
z = c*cos(t)
J = Matrix([
[x.diff(t), x.diff(p)],
[y.diff(t), y.diff(p)],
[z.diff(t), z.diff(p)],
])
print((J.T # J).det().simplify())
This yields
-a**2*b**2*sin(t)**4 + a**2*b**2*sin(t)**2 + a**2*c**2*sin(p)**2*sin(t)**4 - b**2*c**2*sin(p)**2*sin(t)**4 + b**2*c**2*sin(t)**4
and further simplifies to (dividing by (a*b)**2 and taking the sqrt):
sin(t)*np.sqrt(1 + ((c/b)**2*sin(p)**2 + (c/a)**2*cos(p)**2 - 1)*sin(t)**2)
Since for this case the area element is more complex, we can use rejection sampling:
import matplotlib.pyplot as plt
import numpy as np
from numpy import cos, sin
def f_redo(t, p):
return (
sin(t)*np.sqrt(1 + ((c/b)**2*sin(p)**2 + (c/a)**2*cos(p)**2 - 1)*sin(t)**2)
< rng.random(size=t.size)
)
rng = np.random.default_rng(seed=0)
N = 5000
a, b, c = 10, 3, 1
t = rng.uniform(0, np.pi, size=N)
p = rng.uniform(0, 2*np.pi, size=N)
redo = f_redo(t, p)
while redo.any():
t[redo] = rng.uniform(0, np.pi, size=redo.sum())
p[redo] = rng.uniform(0, 2*np.pi, size=redo.sum())
redo[redo] = f_redo(t[redo], p[redo])
x = a*np.sin(t)*np.cos(p)
y = b*np.sin(t)*np.sin(p)
z = c*np.cos(t)
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.scatter(x, y, z, s=2)
plt.show()
which yields the following distribution:
The following plot shows the corresponding one-dimensional projections (x, y, z):
First of all, will appreciate if someone will give me a proper term for "annulus with a shifted hole", see exactly what kind of shape I mean on a picture below.
Back to main question: I want to pick a random point in the orange area, uniform distribution is not required. For a case of a usual annulus I would've picked random point in (r:R) range and a random angle, then transform those to x,y and it's done. But for this unusual shape... is there even a "simple" formula for that, or should I approach it by doing some kind of polygonal approximation of a shape?
I'm interested in a general approach but will appreciate an example in python, javascript or any coding language of your choice.
Here's a simple method that gives a uniform distribution with no resampling.
For simplicity assume that the center of the outer boundary circle (radius r_outer) is at (0, 0) and that the center of the inner circular boundary (radius r_inner) lies at (x_inner, y_inner).
Write D for the outer disk, H1 for the subset of the plane given by the off-center inner hole, and H2 for the central disk of radius r_inner, centered at (0, 0).
Now suppose that we ignore the fact that the inner circle is not central, and instead of sampling from D-H1 we sample from D-H2 (which is easy to do uniformly). Then we've made two mistakes:
there's a region A = H1 - H2 that we might sample from, even though those samples shouldn't be in the result.
there's a region B = H2 - H1 that we never sample from, even though we should
But here's the thing: the regions A and B are congruent: given any point (x, y) in the plane, (x, y) is in H2 if and only if (x_inner - x, y_inner - y) is in H1, and it follows that (x, y) is in A if and only if (x_inner - x, y_inner - y) is in B! The map (x, y) -> (x_inner - x, y_inner - y) represents a rotation by 180 degress around the point (0.5*x_inner, 0.5*y_inner). So there's a simple trick: generate from D - H2, and if we end up with something in H1 - H2, rotate to get the corresponding point of H2 - H1 instead.
Here's the code. Note the use of the square root of a uniform distribution to choose the radius: this is a standard trick. See this article, for example.
import math
import random
def sample(r_outer, r_inner, x_inner, y_inner):
"""
Sample uniformly from (x, y) satisfiying:
x**2 + y**2 <= r_outer**2
(x-x_inner)**2 + (y-y_inner)**2 > r_inner**2
Assumes that the inner circle lies inside the outer circle;
i.e., that hypot(x_inner, y_inner) <= r_outer - r_inner.
"""
# Sample from a normal annulus with radii r_inner and r_outer.
rad = math.sqrt(random.uniform(r_inner**2, r_outer**2))
angle = random.uniform(-math.pi, math.pi)
x, y = rad*math.cos(angle),rad*math.sin(angle)
# If we're inside the forbidden hole, reflect.
if math.hypot(x - x_inner, y - y_inner) < r_inner:
x, y = x_inner - x, y_inner - y
return x, y
And an example plot, generated by the following:
import matplotlib.pyplot as plt
samples = [sample(5, 2, 1.0, 2.0) for _ in range(10000)]
xs, ys = zip(*samples)
plt.scatter(xs, ys, s=0.1)
plt.axis("equal")
plt.show()
Do you really need exact sampling? Because with acceptance/rejection it should work just fine. I assume big orange circle is located at (0,0)
import math
import random
def sample_2_circles(xr, yr, r, R):
"""
R - big radius
r, xr, yr - small radius and its position
"""
x = xr
y = yr
cnd = True
while cnd:
# sample uniformly in whole orange circle
phi = 2.0 * math.pi * random.random()
rad = R * math.sqrt(random.random())
x = rad * math.cos(phi)
y = rad * math.sin(phi)
# check condition - if True we continue in the loop with sampling
cnd = ( (x-xr)**2 + (y-yr)**2 < r*r )
return (x,y)
Since you have shown no equation, algorithm, or code of your own, but just an outline of an algorithm for center-aligned circles, I'll also just give the outline of an algorithm here for the more general case.
The smaller circle is the image of the larger circle under a similarity transformation. I.e. there is a fixed point in the larger circle and a ratio (which is R/r, greater than one) such that you can take any point on the smaller circle, examine the vector from the fixed point to that point, and multiply that vector by the ratio, then the end of that vector when it starts from the fixed point is a point on the larger circle. This transformation is one-to-one.
So you can choose a random point on the smaller circle (choose the angle at random between 0 and two-pi) and choose a ratio at random between 1 and the proportionality ratio R/r between the circles. Then use that the similarity transformation with the same fixed point but using the random ratio to get the image point of the just-chosen point on the smaller circle. This is a random point in your desired region.
This method is fairly simple. In fact, the hardest mathematical part is finding the fixed point of the similarity transformation. But this is pretty easy, given the centers and radii of the two circles. Hint: the transformation takes the center of the smaller circle to the center of the larger circle.
Ask if you need more detail. My algorithm does not yield a uniform distribution: the points will be more tightly packed where the circles are closest together and less tightly packed where the circles are farthest apart.
Here is some untested Python 3.6.2 code that does the above. I'll test it and show a graphic for it when I can.
import math
import random
def rand_pt_between_circles(x_inner,
y_inner,
r_inner,
x_outer,
y_outer,
r_outer):
"""Return a random floating-point 2D point located between the
inner and the outer circles given by their center coordinates and
radii. No error checking is done on the parameters."""
# Find the fixed point of the similarity transformation from the
# inner circle to the outer circle.
x_fixed = x_inner - (x_outer - x_inner) / (r_outer - r_inner) * r_inner
y_fixed = y_inner - (y_outer - y_inner) / (r_outer - r_inner) * r_inner
# Find a a random transformation ratio between 1 and r_outer / r_inner
# and a random point on the inner circle
ratio = 1 + (r_outer - r_inner) * random.random()
theta = 2 * math.pi * random.random()
x_start = x_inner + r_inner * math.cos(theta)
y_start = y_inner + r_inner * math.sin(theta)
# Apply the similarity transformation to the random point.
x_result = x_fixed + (x_start - x_fixed) * ratio
y_result = y_fixed + (y_start - y_fixed) * ratio
return x_result, y_result
The acceptance/rejection method as described by Severin Pappadeux is probably the simplest.
For a direct approach, you can also work in polar coordinates, with the center of the hole as the pole.
The polar equation (Θ, σ) (sorry, no rho) of the external circle will be
(σ cosΘ - xc)² + (σ sinΘ - yc)² = σ² - 2(cosΘ xc + sinΘ yc)σ + xc² + yc² = R²
This is a quadratic equation in σ, that you can easily solve in terms of Θ. Then you can draw an angle in 0, 2π an draw a radius between r and σ.
This won't give you a uniform distribution, because the range of σ is a function of Θ and because of the polar bias. This might be fixed by computing a suitable transfer function, but this is a little technical and probably not tractable analytically.
I wanted to plot the velocity equations of the flow around a rotating cylinder on a polar plot. (The equations are from "Fundamentals of Aerodynamics" by Andersen.) You can see the two equations inside the for loop statements.
I cannot for crying out loud manage to represent the calculated data onto the polar plot. I have tried every idea of mine, but arrived nowhere. I did check the data, and this one seems all correct, as it behaves how it should.
Here the code of my last attempt:
import numpy as np
import matplotlib.pyplot as plt
RadiusColumn = 1.0
VelocityInfinity = 10.0
RPM_Columns = 0.0#
ColumnOmega = (2*np.pi*RPM_Columns)/(60)#rad/s
VortexStrength = 2*np.pi*RadiusColumn**2 * ColumnOmega#rad m^2/s
NumberRadii = 6
NumberThetas = 19
theta = np.linspace(0,2*np.pi,NumberThetas)
radius = np.linspace(RadiusColumn, 10 * RadiusColumn, NumberRadii)
f = plt.figure()
ax = f.add_subplot(111, polar=True)
for r in xrange(len(radius)):
for t in xrange(len(theta)):
VelocityRadius = (1.0 - (RadiusColumn**2/radius[r]**2)) * VelocityInfinity * np.cos(theta[t])
VelocityTheta = - (1.0 + (RadiusColumn**2/radius[r]**2))* VelocityInfinity * np.sin(theta[t]) - (VortexStrength/(2*np.pi*radius[r]))
TotalVelocity = np.linalg.norm((VelocityRadius, VelocityTheta))
ax.quiver(theta[t], radius[r], theta[t] + VelocityTheta/TotalVelocity, radius[r] + VelocityRadius/TotalVelocity)
plt.show()
As you can see, I have set for now the RPMs to 0. That means that the flow should go from left to right, and be symmetric across the horizontal axis. (The flow should go around the cylinder the same way on both sides.) The result however looks more like this:
This is complete nonsense. There seems to be a vorticity, even when there is none set! Even weirder, when I only display data from 0 to pi/2, the flow changes!
As you can see from the code, I have tried to make use of unit vectors, but clearly this is not the way to go. I would appreciate any useful input.
Thanks!
The basic problem is that the .quiver method of a polar Axes object still expects its vector components in Cartesian coordinates, so you need to convert your theta and radial components to x and y yourself:
for r in range(len(radius)):
for t in range(len(theta)):
VelocityRadius = (1.0 - (RadiusColumn**2/radius[r]**2)) * VelocityInfinity * np.cos(theta[t])
VelocityTheta = - (1.0 + (RadiusColumn**2/radius[r]**2))* VelocityInfinity * np.sin(theta[t]) - (VortexStrength/(2*np.pi*radius[r]))
TotalVelocity = np.linalg.norm((VelocityRadius, VelocityTheta))
ax.quiver(theta[t], radius[r],
VelocityRadius/TotalVelocity*np.cos(theta[t])
- VelocityTheta/TotalVelocity*np.sin(theta[t]),
VelocityRadius/TotalVelocity*np.sin(theta[t])
+ VelocityTheta/TotalVelocity*np.cos(theta[t]))
plt.show()
However, you can improve your code a lot by making use of vectorization: you don't need to loop over each point to obtain what you need. So the equivalent of your code, but even clearer:
def pol2cart(th,v_th,v_r):
"""convert polar velocity components to Cartesian, return v_x,v_y"""
return v_r*np.cos(th) - v_th*np.sin(th), v_r*np.sin(th) + v_th*np.cos(th)
theta = np.linspace(0, 2*np.pi, NumberThetas, endpoint=False)
radius = np.linspace(RadiusColumn, 10 * RadiusColumn, NumberRadii)[:,None]
f = plt.figure()
ax = f.add_subplot(111, polar=True)
VelocityRadius = (1.0 - (RadiusColumn**2/radius**2)) * VelocityInfinity * np.cos(theta)
VelocityTheta = - (1.0 + (RadiusColumn**2/radius**2))* VelocityInfinity * np.sin(theta) - (VortexStrength/(2*np.pi*radius))
TotalVelocity = np.linalg.norm([VelocityRadius, VelocityTheta],axis=0)
VelocityX,VelocityY = pol2cart(theta, VelocityTheta, VelocityRadius)
ax.quiver(theta, radius, VelocityX/TotalVelocity, VelocityY/TotalVelocity)
plt.show()
Few notable changes:
I added endpoint=False to theta: since your function is periodic in 2*pi, you don't need to plot the endpoints twice. Note that this means that currently you have more visible arrows; if you want the original behaviour I suggest that you decrease NumberThetas by one.
I added [:,None] to radius: this will make it a 2d array, so later operations in the definition of the velocities will give you 2d arrays: different columns correspond to different angles, different rows correspond to different radii. quiver is compatible with array-valued input, so a single call to quiver will do your work.
Since the velocities are now 2d arrays, we need to call np.linalg.norm on essentially a 3d array, but this works as expected if we specify an axis to work over.
I defined the pol2cart auxiliary function to do the conversion from polar to Cartesian components; this is not necessary but it seems clearer to me this way.
Final remark: I suggest choosing shorter variable names, and ones that don't have CamelCase. That would probably make your coding faster too.
I would like to be able to plot two lines using direction and distance. It is a Drillhole trace, so I have the data in this format right now,
The depth is actually distance down the hole, not vertical depth. Azimuth is from magnetic north. Dip is based on 0 being horizontal. I want to plot two lines from the same point (0,0,0 is fine) and see how they differ, based on this kind of info.
I have no experience with Matplotlib but am comfortable with Python and would like to get to know this plotting tool. I have found this page and it helped to understand the framework, but I still can't figure out how to plot lines with 3d vectors. Can someone give me some pointers on how to do this or where to find the directions I need? Thank you
A script converting your coordinates to cartesian and plotting it with matplotlib with the comments included:
import numpy as np
import matplotlib.pyplot as plt
# import for 3d plot
from mpl_toolkits.mplot3d import Axes3D
# initializing 3d plot
fig = plt.figure()
ax = fig.add_subplot(111, projection = '3d')
# several data points
r = np.array([0, 14, 64, 114])
# get lengths of the separate segments
r[1:] = r[1:] - r[:-1]
phi = np.array([255.6, 255.6, 261.7, 267.4])
theta = np.array([-79.5, -79.5, -79.4, -78.8])
# convert to radians
phi = phi * 2 * np.pi / 360.
# in spherical coordinates theta is measured from zenith down; you are measuring it from horizontal plane up
theta = (90. - theta) * 2 * np.pi / 360.
# get x, y, z from known formulae
x = r*np.cos(phi)*np.sin(theta)
y = r*np.sin(phi)*np.sin(theta)
z = r*np.cos(theta)
# np.cumsum is employed to gradually sum resultant vectors
ax.plot(np.cumsum(x),np.cumsum(y),np.cumsum(z))
For a drillhole with 500 m you may use minimum curvature method, otherwise the position error will be really large. I implemented this in a python module for geostatistics (PyGSLIB). An example showing a complete desurvey process for a real drillhole database, including positions at assay/lithology intervals is shown at:
http://nbviewer.ipython.org/github/opengeostat/pygslib/blob/master/pygslib/Ipython_templates/demo_1.ipynb
This also shows how to export drillholes in VTK format to lad it in paraview.
Results shown in Paraview
The code in Cython to desurvey one interval is as follows:
cpdef dsmincurb( float len12,
float azm1,
float dip1,
float azm2,
float dip2):
"""
dsmincurb(len12, azm1, dip1, azm2, dip2)
Desurvey one interval with minimum curvature
Given a line with length ``len12`` and endpoints p1,p2 with
direction angles ``azm1, dip1, azm2, dip2``, this function returns
the differences in coordinate ``dz,dn,de`` of p2, assuming
p1 with coordinates (0,0,0)
Parameters
----------
len12, azm1, dip1, azm2, dip2: float
len12 is the length between a point 1 and a point 2.
azm1, dip1, azm2, dip2 are direction angles azimuth, with 0 or
360 pointing north and dip angles measured from horizontal
surface positive downward. All these angles are in degrees.
Returns
-------
out : tuple of floats, ``(dz,dn,de)``
Differences in elevation, north coordinate (or y) and
east coordinate (or x) in an Euclidean coordinate system.
See Also
--------
ang2cart,
Notes
-----
The equations were derived from the paper:
http://www.cgg.com/data//1/rec_docs/2269_MinimumCurvatureWellPaths.pdf
The minimum curvature is a weighted mean based on the
dog-leg (dl) value and a Ratio Factor (rf = 2*tan(dl/2)/dl )
if dl is zero we assign rf = 1, which is equivalent to balanced
tangential desurvey method. The dog-leg is zero if the direction
angles at the endpoints of the desurvey intervals are equal.
Example
--------
>>> dsmincurb(len12=10, azm1=45, dip1=75, azm2=90, dip2=20)
(7.207193374633789, 1.0084573030471802, 6.186459064483643)
"""
# output
cdef:
float dz
float dn
float de
# internal
cdef:
float i1
float a1
float i2
float a2
float DEG2RAD
float rf
float dl
DEG2RAD=3.141592654/180.0
i1 = (90 - dip1) * DEG2RAD
a1 = azm1 * DEG2RAD
i2 = (90 - dip2) * DEG2RAD
a2 = azm2 * DEG2RAD
# calculate the dog-leg (dl) and the Ratio Factor (rf)
dl = acos(cos(i2-i1)-sin(i1)*sin(i2)*(1-cos(a2-a1)))
if dl!=0.:
rf = 2*tan(dl/2)/dl # minimum curvature
else:
rf=1 # balanced tangential
dz = 0.5*len12*(cos(i1)+cos(i2))*rf
dn = 0.5*len12*(sin(i1)*cos(a1)+sin(i2)*cos(a2))*rf
de = 0.5*len12*(sin(i1)*sin(a1)+sin(i2)*sin(a2))*rf
return dz,dn,de