I have been not using math for a long time and this should be a simple problem to solve.
Suppose I have two points A: (1, 0) and B: (1, -1).
I want to use a program (Python or whatever programming language) to calculate the clockwise angle between A, origin (0, 0) and B. It will be something like this:
angle_clockwise(point1, point2)
Note that the order of the parameters matters. Since the angle calculation will be clockwise:
If I call angle_clockwise(A, B), it returns 45.
If I call angle_clockwise(B, A), it returns 315.
In other words, the algorithm is like this:
Draw a line (line 1) between the first point param with (0, 0).
Draw a line (line 2) between the second point param with (0, 0).
Revolve line 1 around (0, 0) clockwise until it overlaps line 2.
The angular distance line 1 traveled will be the returned angle.
Is there any way to code this problem?
Numpy's arctan2(y, x) will compute the counterclockwise angle (a value in radians between -π and π) between the origin and the point (x, y).
You could do this for your points A and B, then subtract the second angle from the first to get the signed clockwise angular difference. This difference will be between -2π and 2π, so in order to get a positive angle between 0 and 2π you could then take the modulo against 2π. Finally you can convert radians to degrees using np.rad2deg.
import numpy as np
def angle_between(p1, p2):
ang1 = np.arctan2(*p1[::-1])
ang2 = np.arctan2(*p2[::-1])
return np.rad2deg((ang1 - ang2) % (2 * np.pi))
For example:
A = (1, 0)
B = (1, -1)
print(angle_between(A, B))
# 45.
print(angle_between(B, A))
# 315.
If you don't want to use numpy, you could use math.atan2 in place of np.arctan2, and use math.degrees (or just multiply by 180 / math.pi) in order to convert from radians to degrees. One advantage of the numpy version is that you can also pass two (2, ...) arrays for p1 and p2 in order to compute the angles between multiple pairs of points in a vectorized way.
Use the inner product and the determinant of the two vectors. This is really what you should understand if you want to understand how this works. You'll need to know/read about vector math to understand.
See: https://en.wikipedia.org/wiki/Dot_product and https://en.wikipedia.org/wiki/Determinant
from math import acos
from math import sqrt
from math import pi
def length(v):
return sqrt(v[0]**2+v[1]**2)
def dot_product(v,w):
return v[0]*w[0]+v[1]*w[1]
def determinant(v,w):
return v[0]*w[1]-v[1]*w[0]
def inner_angle(v,w):
cosx=dot_product(v,w)/(length(v)*length(w))
rad=acos(cosx) # in radians
return rad*180/pi # returns degrees
def angle_clockwise(A, B):
inner=inner_angle(A,B)
det = determinant(A,B)
if det<0: #this is a property of the det. If the det < 0 then B is clockwise of A
return inner
else: # if the det > 0 then A is immediately clockwise of B
return 360-inner
In the determinant computation, you're concatenating the two vectors to form a 2 x 2 matrix, for which you're computing the determinant.
Here's a solution that doesn't require cmath.
import math
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
v1 = Vector(0, 1)
v2 = Vector(0, -1)
v1_theta = math.atan2(v1.y, v1.x)
v2_theta = math.atan2(v2.y, v2.x)
r = (v2_theta - v1_theta) * (180.0 / math.pi)
if r < 0:
r += 360.0
print r
A verified 0° to 360° solution
It is an old thread, but for me the other solutions didn't work well, so I implemented my own version.
My function will return a number between 0 and 360 (excluding 360) for two points on the screen (i.e. 'y' starts at the top and increasing towards the bottom), where results are as in a compass, 0° at the top, increasing clockwise:
def angle_between_points(p1, p2):
d1 = p2[0] - p1[0]
d2 = p2[1] - p1[1]
if d1 == 0:
if d2 == 0: # same points?
deg = 0
else:
deg = 0 if p1[1] > p2[1] else 180
elif d2 == 0:
deg = 90 if p1[0] < p2[0] else 270
else:
deg = math.atan(d2 / d1) / pi * 180
lowering = p1[1] < p2[1]
if (lowering and deg < 0) or (not lowering and deg > 0):
deg += 270
else:
deg += 90
return deg
Check out the cmath python library.
>>> import cmath
>>> a_phase = cmath.phase(complex(1,0))
>>> b_phase = cmath.phase(complex(1,-1))
>>> (a_phase - b_phase) * 180 / cmath.pi
45.0
>>> (b_phase - a_phase) * 180 / cmath.pi
-45.0
You can check if a number is less than 0 and add 360 to it if you want all positive angles, too.
Chris St Pierre: when using your function with:
A = (x=1, y=0)
B = (x=0, y=1)
This is supposed to be a 90 degree angle from A to B. Your function will return 270.
Is there an error in how you process the sign of the det or am I missing something?
A formula that calculates an angle clockwise, and is used in surveying:
f(E,N)=pi()-pi()/2*(1+sign(N))* (1-sign(E^2))-pi()/4*(2+sign(N))*sign(E)
-sign(N*E)*atan((abs(N)-abs(E))/(abs(N)+abs(E)))
The formula gives angles from 0 to 2pi,start from the North and
is working for any value of N and E. (N=N2-N1 and E=E2-E1)
For N=E=0 the result is undefined.
in radians, clockwise, from 0 to PI * 2
static angle(center:Coord, p1:Coord, p2:Coord) {
var a1 = Math.atan2(p1.y - center.y, p1.x - center.x);
var a2 = Math.atan2(p2.y - center.y, p2.x -center.x);
a1 = a1 > 0 ? a1 : Math.PI * 2 + a1;//make angle from 0 to PI * 2
a2 = a2 > 0 ? a2 : Math.PI * 2 + a2;
if(a1 > a2) {
return a1 - a2;
} else {
return Math.PI * 2 - (a2 - a1)
}
}
Related
I am try to find the angle of line
i know the coordinate points
Start Point : 404, 119
Mid Point : 279, 214
End Point : 154, 310
import numpy as np
def findangle(x1,y1,x2,y2,x3,y3):
ria = np.arctan2(y2 - y1, x2 - x1) - np.arctan2(y3 - y1, x3 - x1)
webangle = int(np.abs(ria * 180 / np.pi))
return webangle
result
Its return 270. But actual angle is 85-90.
Now, I want formula to calculate the angle (Either i Will rotate the image clockwise or Anticlockwise that time also return actual angle) in python code
I think you need to adjust the returned angle based on the range you wish the result to be in.
The following assumes you desire results between -180 (inclusive) and 180 (exclusive).
def adjust(a, degrees=True):
v = 180 if degrees else np.pi
return (a + v) % (2 * v) - v
def angle(p, degrees=True):
assert p.shape == (3, 2), 'p should have 3 points, in 2D'
yx = p[:, ::-1]
a0 = np.arctan2(*(yx[1] - yx[0]))
a1 = np.arctan2(*(yx[2] - yx[1]))
a = np.rad2deg(a1 - a0) if degrees else a1 - a0
return adjust(a, degrees)
And here is some code to test validity:
def rot(a, degrees=True):
a = np.deg2rad(a) if degrees else a
sa, ca = np.sin(a), np.cos(a)
return np.array([[ca, sa], [-sa, ca]])
def genp(angle, initial=0, lengths=(1, 1), degrees=True):
p0 = np.array((0, 0))
p1 = np.array((lengths[0], 0))
p2 = p1 + np.array((lengths[1], 0)) # rot(angle, degrees=degrees)
p = np.vstack((p0, p1, p2)) # rot(initial, degrees=degrees)
return p
def plot_problem(p, ax=None):
ax = plt.gca() if ax is None else ax
ax.plot(*p.T, '-o')
for s, xy in zip(list('abc'), p):
ax.annotate(s, xy, xytext=(0, 5), textcoords='offset points')
ax.set_aspect('equal')
Example:
p = genp(60, initial=30, lengths=(2, 1))
>>> angle(p)
60.0
plot_problem(p)
Now, a stress test:
n = 1000
np.random.seed(0)
for _ in range(n):
a = np.random.uniform(-180, 180)
initial = np.random.uniform(-180, 180)
lengths = np.random.uniform(1, 10, size=2)
p = genp(a, initial, lengths)
ahat = angle(p)
assert np.allclose(a, ahat)
Which passes.
It's a simple math. The numpy.arctan2() returns a value in the range [-π, π]. So a difference of two values is in the range [-2π, 2π], and the absolute value of that is in the range [0, 2π]. You want an angle x in the range [0, π], and x is equivalent to (2π - x) in your context. So you can take x if x < π, and take (2π - x) otherwise.
So, just do like this using min() for example.
def findangle(x1,y1,x2,y2,x3,y3):
...
return min(webangle, 360 - webangle)
As a side note, you don't need to use Numpy functions for a scalar input. Just use the math.atan2() and math.fabs().
I have a simple 2D ray-casting routine that gets terribly slow as soon as the number of obstacles increases.
This routine is made up of:
2 for loops (outer loop iterates over each ray/direction, then inner loop iterates over each line obstacle)
multiple if statements (check if a value is > or < than another value or if an array is empty)
Question: How can I condense all these operations into 1 single block of vectorized instructions using Numpy ?
More specifically, I am facing 2 issues:
I have managed to vectorize the inner loop (intersection between a ray and each obstacle) but I am unable to run this operation for all rays at once.
The only workaround I found to deal with the if statements is to use masked arrays. Something tells me it is not the proper way to handle these statements in this case (it seems clumsy, cumbersome and unpythonic)
Original code:
from math import radians, cos, sin
import matplotlib.pyplot as plt
import numpy as np
N = 10 # dimensions of canvas (NxN)
sides = np.array([[0, N, 0, 0], [0, N, N, N], [0, 0, 0, N], [N, N, 0, N]])
edges = np.random.rand(5, 4) * N # coordinates of 5 random segments (x1, x2, y1, y2)
edges = np.concatenate((edges, sides))
center = np.array([N/2, N/2]) # coordinates of center point
directions = np.array([(cos(radians(a)), sin(radians(a))) for a in range(0, 360, 10)]) # vectors pointing in all directions
intersections = []
# for each direction
for d in directions:
min_dist = float('inf')
# for each edge
for e in edges:
p1x, p1y = e[0], e[2]
p2x, p2y = e[1], e[3]
p3x, p3y = center
p4x, p4y = center + d
# find intersection point
den = (p1x - p2x) * (p3y - p4y) - (p1y - p2y) * (p3x - p4x)
if den:
t = ((p1x - p3x) * (p3y - p4y) - (p1y - p3y) * (p3x - p4x)) / den
u = -((p1x - p2x) * (p1y - p3y) - (p1y - p2y) * (p1x - p3x)) / den
# if any:
if t > 0 and t < 1 and u > 0:
sx = p1x + t * (p2x - p1x)
sy = p1y + t * (p2y - p1y)
isec = np.array([sx, sy])
dist = np.linalg.norm(isec-center)
# make sure to select the nearest one (from center)
if dist < min_dist:
min_dist = dist
nearest = isec
# store nearest interesection point for each ray
intersections.append(nearest)
# Render
plt.axis('off')
for x, y in zip(edges[:,:2], edges[:,2:]):
plt.plot(x, y)
for isec in np.array(intersections):
plt.plot((center[0], isec[0]), (center[1], isec[1]), '--', color="#aaaaaa", linewidth=.8)
Vectorized version (attempt):
from math import radians, cos, sin
import matplotlib.pyplot as plt
from scipy import spatial
import numpy as np
N = 10 # dimensions of canvas (NxN)
sides = np.array([[0, N, 0, 0], [0, N, N, N], [0, 0, 0, N], [N, N, 0, N]])
edges = np.random.rand(5, 4) * N # coordinates of 5 random segments (x1, x2, y1, y2)
edges = np.concatenate((edges, sides))
center = np.array([N/2, N/2]) # coordinates of center point
directions = np.array([(cos(radians(a)), sin(radians(a))) for a in range(0, 360, 10)]) # vectors pointing in all directions
intersections = []
# Render edges
plt.axis('off')
for x, y in zip(edges[:,:2], edges[:,2:]):
plt.plot(x, y)
# for each direction
for d in directions:
p1x, p1y = edges[:,0], edges[:,2]
p2x, p2y = edges[:,1], edges[:,3]
p3x, p3y = center
p4x, p4y = center + d
# denominator
den = (p1x - p2x) * (p3y - p4y) - (p1y - p2y) * (p3x - p4x)
# first 'if' statement -> if den > 0
mask = den > 0
den = den[mask]
p1x = p1x[mask]
p1y = p1y[mask]
p2x = p2x[mask]
p2y = p2y[mask]
t = ((p1x - p3x) * (p3y - p4y) - (p1y - p3y) * (p3x - p4x)) / den
u = -((p1x - p2x) * (p1y - p3y) - (p1y - p2y) * (p1x - p3x)) / den
# second 'if' statement -> if (t>0) & (t<1) & (u>0)
mask2 = (t > 0) & (t < 1) & (u > 0)
t = t[mask2]
p1x = p1x[mask2]
p1y = p1y[mask2]
p2x = p2x[mask2]
p2y = p2y[mask2]
# x, y coordinates of all intersection points in the current direction
sx = p1x + t * (p2x - p1x)
sy = p1y + t * (p2y - p1y)
pts = np.c_[sx, sy]
# if any:
if pts.size > 0:
# find nearest intersection point
tree = spatial.KDTree(pts)
nearest = pts[tree.query(center)[1]]
# Render
plt.plot((center[0], nearest[0]), (center[1], nearest[1]), '--', color="#aaaaaa", linewidth=.8)
Reformulation of the problem – Finding the intersection between a line segment and a line ray
Let q and q2 be the endpoints of a segment (obstacle). For convenience let's define a class to represent points and vectors in the plane. In addition to the usual operations, a vector multiplication is defined by u × v = u.x * v.y - u.y * v.x.
Caution: here Coord(2, 1) * 3 returns Coord(6, 3) while Coord(2, 1) * Coord(-1, 4) outputs 9. To avoid this confusion it might have been possible to restrict * to the scalar multiplication and use ^ via __xor__ for the vector multiplication.
class Coord:
def __init__(self, x, y):
self.x = x
self.y = y
#property
def radius(self):
return np.sqrt(self.x ** 2 + self.y ** 2)
def _cross_product(self, other):
assert isinstance(other, Coord)
return self.x * other.y - self.y * other.x
def __mul__(self, other):
if isinstance(other, Coord):
# 2D "cross"-product
return self._cross_product(other)
elif isinstance(other, int) or isinstance(other, float):
# scalar multiplication
return Coord(self.x * other, self.y * other)
def __rmul__(self, other):
return self * other
def __sub__(self, other):
return Coord(self.x - other.x, self.y - other.y)
def __add__(self, other):
return Coord(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Coord({self.x}, {self.y})"
Now, I find it easier to handle a ray in polar coordinates: For a given angle theta (direction) the goal is to determine if it intersects the segment, and if so determine the corresponding radius. Here is a function to find that. See here for an explanation of why and how. I tried to use the same variable names as in the previous link.
def find_intersect_btw_ray_and_sgmt(q, q2, theta):
"""
Args:
q (Coord): first endpoint of the segment
q2 (Coord): second endpoint of the segment
theta (float): angle of the ray
Returns:
(float): np.inf if the ray does not intersect the segment,
the distance from the origin of the intersection otherwise
"""
assert isinstance(q, Coord) and isinstance(q2, Coord)
s = q2 - q
r = Coord(np.cos(theta), np.sin(theta))
cross = r * s # 2d cross-product
t_num = q * s
u_num = q * r
## the intersection point is roughly at a distance t_num / cross
## from the origin. But some cases must be checked beforehand.
## (1) the segment [PQ2] is aligned with the ray
if np.isclose(cross, 0) and np.isclose(u_num, 0):
return min(q.radius, q2.radius)
## (2) the segment [PQ2] is parallel with the ray
elif np.isclose(cross, 0):
return np.inf
t, u = t_num / cross, u_num / cross
## There is actually an intersection point
if t >= 0 and 0 <= u <= 1:
return t
## (3) No intersection point
return np.inf
For instance find_intersect_btw_ray_and_sgmt(Coord(1, 2), Coord(-1, 2), np.pi / 2) should returns 2.
Note that here for simplicity, I only considered the case where the origin of the rays is at Coord(0, 0). This can be easily extended to the general case by setting t_num = (q - origin) * s and u_num = (q - origin) * r.
Let's vectorize it!
What is very interesting here is that the operations defined in the Coord class also apply to cases where x and y are numpy arrays! Hence applying any defined operation on Coord(np.array([1, 2, 0]), np.array([2, -1, 3])) amounts applying it elementwise to the points (1, 2), (2, -1) and (0, 3). The operations of Coord are therefore already vectorized. The constructor can be modified into:
def __init__(self, x, y):
x, y = np.array(x), np.array(y)
assert x.shape == y.shape
self.x, self.y = x, y
self.shape = x.shape
Now, we would like the function find_intersect_btw_ray_and_sgmt to be able to handle the case where the parameters q and q2contains sequences of endpoints. Before the sanity checks, all the operations are working properly since, as we have mentioned, they are already vectorized. As you mentionned the conditional statements can be "vectorized" using masks. Here is what I propose:
def find_intersect_btw_ray_and_sgmts(q, q2, theta):
assert isinstance(q, Coord) and isinstance(q2, Coord)
assert q.shape == q2.shape
EPS = 1e-14
s = q2 - q
r = Coord(np.cos(theta), np.sin(theta))
cross = r * s
cross_sign = np.sign(cross)
cross = cross * cross_sign
t_num = (q * s) * cross_sign
u_num = (q * r) * cross_sign
radii = np.zeros_like(t_num)
mask = ~np.isclose(cross, 0) & (t_num >= -EPS) & (-EPS <= u_num) & (u_num <= cross + EPS)
radii[~mask] = np.inf # no intersection
radii[mask] = t_num[mask] / cross[mask] # intersection
return radii
Note that cross, t_num and u_num are multiplied by the sign of cross to ensure that the division by cross keeps the sign of the dividends. Hence conditions of the form ((t_num >= 0) & (cross >= 0)) | ((t_num <= 0) & (cross <= 0)) can be replaced by (t_num >= 0).
For simplicity, we omitted the case (1) where the radius and the segment were aligned ((cross == 0) & (u_num == 0)). This could be incorporated by carefully adding a second mask.
For a given value of theta, we are able to determine if the corresponing ray intersects with several segments at once.
## Some useful functions
def polar_to_cartesian(r, theta):
return Coord(r * np.cos(theta), r * np.sin(theta))
def plot_segments(p, q, *args, **kwargs):
plt.plot([p.x, q.x], [p.y, q.y], *args, **kwargs)
def plot_rays(radii, thetas, *args, **kwargs):
endpoints = polar_to_cartesian(radii, thetas)
n = endpoints.shape
origin = Coord(np.zeros(n), np.zeros(n))
plot_segments(origin, endpoints, *args, **kwargs)
## Data generation
M = 5 # size of the canvas
N = 10 # number of segments
K = 16 # number of rays
q = Coord(*np.random.uniform(-M/2, M/2, size=(2, N)))
p = q + Coord(*np.random.uniform(-M/2, M/2, size=(2, N)))
thetas = np.linspace(0, 2 * np.pi, K, endpoint=False)
## For each ray, find the minimal distance of intersection
## with all segments
plt.figure(figsize=(5, 5))
plot_segments(p, q, "royalblue", marker=".")
for theta in thetas:
radii = find_intersect_btw_ray_and_sgmts(p, q, theta)
radius = np.min(radii)
if not np.isinf(radius):
plot_rays(radius, theta, color="orange")
else:
plot_rays(2*M, theta, ':', c='orange')
plt.plot(0, 0, 'kx')
plt.xlim(-M, M)
plt.ylim(-M, M)
And that's not all! Thanks to the broadcasting of python, it is possible to avoid iteration on theta values. For example, recall that np.array([1, 2, 3]) * np.array([[1], [2], [3], [4]]) produces a matrix of size 4 × 3 of the pairwise products. In the same way Coord([[5],[7]], [[5],[1]]) * Coord([2, 4, 6], [-2, 4, 0]) outputs a 2 × 3 matrix containing all the pairwise cross product between vectors (5, 5), (7, 1) and (2, -2), (4, 4), (6, 0).
Finally, the intersections can be determined in the following way:
radii_all = find_intersect_btw_ray_and_sgmts(p, q, np.vstack(thetas))
# p and q have a shape of (N,) and np.vstack(thetas) of (K, 1)
# this radii_all have a shape of (K, N)
# radii_all[k, n] contains the distance from the origin of the intersection
# between k-th ray and n-th segment (or np.inf if there is no intersection point)
radii = np.min(radii_all, axis=1)
# radii[k] contains the distance from the origin of the closest intersection
# between k-th ray and all segments
do_intersect = ~np.isinf(radii)
plot_rays(radii[do_intersect], thetas[do_intersect], color="orange")
plot_rays(2*M, thetas[~do_intersect], ":", color="orange")
In a game I'm writing with Pygame, I have a 2D point (x,y) in a box from (0,0) to (1,1) (perfect square with side length 1).
I want to calculate the euclidean distance from the point to the box boundary in a direction alpha, where alpha is an azimuth direction measured in radians counter-clockwise, starting from alpha=0 as the x-axis direction.
I wrote a python code that calculates this distance, but I'm not sure it's the most efficient/cleanest way:
import numpy as np
def get_distance_to_boundary(x, y, angle):
cos_angle, sin_angle = np.cos(angle), np.sin(angle)
if cos_angle == 0:
return y if sin_angle < 0 else 1 - y
if sin_angle == 0:
return x if cos_angle < 0 else 1 - x
x_target = 1 if cos_angle > 0 else 0
y_target = y + (x_target - x) * np.tan(angle)
if y_target > 1:
return np.fabs((1 - y) / sin_angle)
if y_target < 0:
return np.fabs(y / sin_angle)
return np.sqrt((x_target - x) ** 2 + (y_target - y) ** 2)
Any idea for a better approach?
Illustration:
This method is a little more efficient/cleaner because you don't need tan, fabs, sqrt or **2:
def distance(x,y,angle):
cos_angle, sin_angle = np.cos(angle), np.sin(angle)
if cos_angle == 0:
return y if sin_angle < 0 else 1 - y
if sin_angle == 0:
return x if cos_angle < 0 else 1 - x
distance_EW = (1-x)/cos_angle if cos_angle>0 else -x/cos_angle
distance_NS = (1-y)/sin_angle if sin_angle>0 else -y/sin_angle
return min(distance_EW, distance_NS)
I define distance_EW as the distance in the case where the target is on the East wall (if cos_angle>0) or on the West wall (if cos_angle<0). Similarly, define distance_NS for the North or South wall.
...
WARNING: My distance function will sometimes produce different results than your function because of rounding errors!! This is especially problematic when your starting point is at the border of the box and angle is close to a multiple of π/2.
I would suggest you set some sort of tolerance like if abs(sin_angle) < 1e-12:, instead of if sin_angle == 0:. That way, sin_angle = np.sin(np.pi) will be accepted in the if condition, even though it is not exactly equal to 0 (because np.sin(np.pi) is 1.2e-16 with python).
I'd like to place 4 points around a point on a sphere (cartesian coordinates: x y z), it doesn't matter how far these 4 points are from the center point (straight line distance or spherical distance) but I'd like these 4 points to be the same distance D from the center point (ideally the 5 points should have a + or x shape, so one north, one south, one east and one south).
I could do it by changing one variable (x, y or z) then keeping another the same and calculating the last variable based on the formula x * x + y * y + z * z = radius * radius but that didn't give good results. I could also maybe use the pythagorean theorem to get the distance between each of the 4 points and the center but I think there is a better formula that I don't know (and couldn't find by doing my research).
Thank you.
Some math
AFAIU your problem is that you have a sphere and a point on the sphere and you want to add 4 more points on the same sphere that would form a kind of a cross on the surface of the sphere around the target point.
I think it is easier to think about this problem in terms of vectors. You have a vector from the center of the sphere to your target point V of size R. All the point lying on the distance d from the target point form another sphere. The crossing of two sphere is a circle. Obviously this circle lies in a plane that is orthogonal to V. Solving a simple system of equations you can find that the distance from the target point to that plane is d^2/(2*R). So the vector from the center of the original sphere to the center of the circle:
Vc = V * (1 - d^2/(2*R^2))
and the radius of that circle is
Rc = sqrt(d^2 - (d^2/(2*R))**2)
So now to select 4 points, you need to select two orthogonal unit vectors lying in that plane D1 and D2. Then 4 points would be Vc + Rc*D1, Vc - Rc*D1, Vc + Rc*D2, and Vc - Rc*D2. To do this you may first select D1 fixing z =0 and switch x and y in Vc
D1 = (Vy/sqrt(Vx^2+Vy^2), -Vx/sqrt(Vx^2+Vy^2), 0)
and then find D2 as a result of cross-product of V and D1. This will work unless unless Vx = Vy = 0 (i.e. V goes along the z-axis) but in that case you can select
D1 = (1,0,0)
D2 = (0,1,0)
Some code
And here is some Python code that implements that math:
def cross_product(v1, v2):
return (v1[1] * v2[2] - v1[2] * v2[1],
v1[2] * v2[0] - v1[0] * v2[2],
v1[0] * v2[1] - v1[1] * v2[0])
def find_marks(sphereCenter, target, d):
lsc = list(sphereCenter)
lt0 = list(target)
lt1 = map(lambda c1, c0: (c1 - c0), lt0, lsc) # shift everything as if sphereCenter is (0,0,0)
rs2 = sum(map(lambda x: x ** 2, lt1)) # spehere radius**2
rs = rs2 ** 0.5
dv = d ** 2 / 2.0 / rs
dvf = d ** 2 / 2.0 / rs2
lcc = map(lambda c: c * (1 - dvf), lt1) # center of the circle in the orthogonal plane
rc = (d ** 2 - dv ** 2) ** 0.5 # orthogonal circle radius
relEps = 0.0001
absEps = relEps * rs
dir1 = (lt1[1], -lt1[0], 0) # select any direction orthogonal to the original vector
dl1 = (lt1[0] ** 2 + lt1[1] ** 2) ** 0.5
# if original vector is (0,0, z) then we've got dir1 = (0,0,0) but we can use (1,0,0) as our vector
if abs(dl1) < absEps:
dir1 = (rc, 0, 0)
dir2 = (0, rc, 0)
else:
dir1 = map(lambda c: rc * c / dl1, dir1)
dir2 = cross_product(lt1, dir1)
dl2 = sum(map(lambda c: c ** 2, dir2)) ** 0.5
dir2 = map(lambda c: rc * c / dl2, dir2)
p1 = map(lambda c0, c1, c2: c0 + c1 + c2, lsc, lcc, dir1)
p2 = map(lambda c0, c1, c2: c0 + c1 + c2, lsc, lcc, dir2)
p3 = map(lambda c0, c1, c2: c0 + c1 - c2, lsc, lcc, dir1)
p4 = map(lambda c0, c1, c2: c0 + c1 - c2, lsc, lcc, dir2)
return [tuple(p1), tuple(p2), tuple(p3), tuple(p4)]
For an extreme case
find_marks((0, 0, 0), (12, 5, 0), 13.0 * 2 ** 0.5)
i.e. for a circle of radius 13 with a center at (0,0,0), the target point lying on the big circle in the plane parallel to the xy-plane and d = sqrt(2)*R, the answer is
[(4.999999999999996, -12.000000000000004, 0.0),
(-5.329070518200751e-15, -2.220446049250313e-15, -13.0),
(-5.000000000000006, 12.0, 0.0),
(-5.329070518200751e-15, -2.220446049250313e-15, 13.0)]
So two points (2-nd and 4-th) are just two z-extremes and the other two are 90° rotations of the target point in the xy-plane which looks quite OK.
For a less extreme example:
find_marks((1, 2, 3), (13, 7, 3), 1)
which is the previous example with d reduced to 1 and with the original center moved to (1,2,3)
[(13.34882784191617, 6.06281317940119, 3.0),
(12.964497041420119, 6.985207100591716, 2.000739918710263),
(12.580166240924067, 7.907601021782242, 3.0),
(12.964497041420119, 6.985207100591716, 3.999260081289737)]
which also looks plausible
You are confronted with an enemy within a rectangular shaped room and you've got only a laser beam weapon, the room has no obstructions in it and the walls can completely reflect the laser beam. However the laser can only travels a certain distance before it become useless and if it hit a corner it would reflect back in the same direction it came from.
That's how the puzzle goes and you are given the coordinates of your location and the target's location, the room dimensions and the maximum distance the beam can travel. for example If the room is 3 by 2 and your location is (1, 1) and the target is (2, 1) then the possible solutions are:
I tried the following approach, start from the source (1, 1) and create a vector at angle 0 radians, trace the vector path and reflections until either it hits the target or the total length of the vectors exceeds the max allowed length, repeat with 0.001 radians interval until it completes a full cycle. This the code I have so far:
from math import *
UPRIGHT = 0
DOWNRIGHT = 1
DOWNLEFT = 2
UPLEFT = 3
UP = 4
RIGHT = 5
LEFT = 6
DOWN = 7
def roundDistance (a):
b = round (a * 100000)
return b / 100000.0
# only used for presenting and doesn't affect percision
def double (a):
b = round (a * 100)
if b / 100.0 == b: return int (b)
return b / 100.0
def roundAngle (a):
b = round (a * 1000)
return b / 1000.0
def isValid (point):
x,y = point
if x < 0 or x > width or y < 0 or y > height: return False
return True
def isCorner (point):
if point in corners: return True
return False
# Find the angle direction in relation to the origin (observer) point
def getDirection (a):
angle = roundAngle (a)
if angle == 0: return RIGHT
if angle > 0 and angle < pi / 2: return UPRIGHT
if angle == pi / 2: return UP
if angle > pi / 2 and angle < pi: return UPLEFT
if angle == pi: return LEFT
if angle > pi and angle < 3 * pi / 2: return DOWNLEFT
if angle == 3 * pi / 2: return DOWN
return DOWNRIGHT
# Measure reflected vector angle
def getReflectionAngle (tail, head):
v1 = (head[0] - tail[0], head[1] - tail[1])
vx,vy = v1
n = (0, 0)
# Determin the normal vector from the tail's position on the borders
if head[0] == 0: n = (1, 0)
if head[0] == width: n = (-1, 0)
if head[1] == 0: n = (0, 1)
if head[1] == height: n = (0, -1)
nx,ny = n
# Calculate the reflection vector using the formula:
# r = v - 2(v.n)n
r = (vx * (1 - 2 * nx * nx), vy * (1 - 2 * ny * ny))
# calculating the angle of the reflection vector using it's a and b values
# if b (adjacent) is zero that means the angle is either pi/2 or -pi/2
if r[0] == 0:
return pi / 2 if r[1] >= 0 else 3 * pi / 2
return (atan2 (r[1], r[0]) + (2 * pi)) % (2 * pi)
# Find the intersection point between the vector and borders
def getIntersection (tail, angle):
if angle < 0:
print "Negative angle: %f" % angle
direction = getDirection (angle)
if direction in [UP, RIGHT, LEFT, DOWN]: return None
borderX, borderY = corners[direction]
x0,y0 = tail
opp = borderY - tail[1]
adj = borderX - tail[0]
p1 = (x0 + opp / tan (angle), borderY)
p2 = (borderX, y0 + adj * tan (angle))
if isValid (p1) and isValid (p2):
print "Both intersections are valid: ", p1, p2
if isValid (p1) and p1 != tail: return p1
if isValid (p2) and p2 != tail: return p2
return None
# Check if the vector pass through the target point
def isHit (tail, head):
d = calcDistance (tail, head)
d1 = calcDistance (target, head)
d2 = calcDistance (target, tail)
return roundDistance (d) == roundDistance (d1 + d2)
# Measure distance between two points
def calcDistance (p1, p2):
x1,y1 = p1
x2,y2 = p2
return ((y2 - y1)**2 + (x2 - x1)**2)**0.5
# Trace the vector path and reflections and check if it can hit the target
def rayTrace (point, angle):
path = []
length = 0
tail = point
path.append ([tail, round (degrees (angle))])
while length < maxLength:
head = getIntersection (tail, angle)
if head is None:
#print "Direct reflection at angle (%d)" % angle
return None
length += calcDistance (tail, head)
if isHit (tail, head) and length <= maxLength:
path.append ([target])
return [path, double (length)]
if isCorner (head):
#print "Corner reflection at (%d, %d)" % (head[0], head[1])
return None
p = (double (head[0]), double (head[1]))
path.append ([p, double (degrees (angle))])
angle = getReflectionAngle (tail, head)
tail = head
return None
def solve (w, h, po, pt, m):
# Initialize global variables
global width, height, origin, target, maxLength, corners, borders
width = w
height = h
origin = po
target = pt
maxLength = m
corners = [(w, h), (w, 0), (0, 0), (0, h)]
angle = 0
solutions = []
# Loop in anti-clockwise direction for one cycle
while angle < 2 * pi:
angle += 0.001
path = rayTrace (origin, angle)
if path is not None:
# extract only the points coordinates
route = [x[0] for x in path[0]]
if route not in solutions:
solutions.append (route)
print path
# Anser is 7
solve (3, 2, (1, 1), (2, 1), 4)
# Answer is 9
#solve (300, 275, (150, 150), (185, 100), 500)
The code works somehow but it doesn't find all the possible solutions, I have a big precision problem in it, I dont' know how many decimals should I consider when comparing distances or angles. I'm not sure it's the right way to do it but that's the best I was able to do.
How can I fix my code to extract all solutions? I need it to be efficient because the room can get quite large (500 x 500). Is there a better way or maybe some sort of algorithm to do this?
what if you started by mirroring the target at all the walls; then mirror the mirror images at all the walls and so on until the distance gets too big for the laser to reach the target? any laser shot in any direction of a target mirrored that way will hit said target. (this is my comment from above; repeated here to make answer more self-contained...)
this is the mirroring part of the answer: get_mirrored will return the four mirror images of point with the mirror-box limited by BOTTOM_LEFT and TOP_RIGHT.
BOTTOM_LEFT = (0, 0)
TOP_RIGHT = (3, 2)
SOURCE = (1, 1)
TARGET = (2, 1)
def get_mirrored(point):
ret = []
# mirror at top wall
ret.append((point[0], point[1] - 2*(point[1] - TOP_RIGHT[1])))
# mirror at bottom wall
ret.append((point[0], point[1] - 2*(point[1] - BOTTOM_LEFT[1])))
# mirror at left wall
ret.append((point[0] - 2*(point[0] - BOTTOM_LEFT[0]), point[1]))
# mirror at right wall
ret.append((point[0] - 2*(point[0] - TOP_RIGHT[0]), point[1]))
return ret
print(get_mirrored(TARGET))
this will return the 4 mirror images of the given point:
[(2, 3), (2, -1), (-2, 1), (4, 1)]
which is the target mirrored one time.
then you could iterate that until all the mirrored targets are out of range. all the mirror images within range will give you a direction in which to point your laser.
this is a way how you could iteratively get to the mirrored targets within a given DISTANCE
def get_targets(start_point, distance):
all_targets = set((start_point, )) # will also be the return value
last_targets = all_targets # need to memorize the new points
while True:
new_level_targets = set() # if this is empty: break the loop
for tgt in last_targets: # loop over what the last iteration found
new_targets = get_mirrored(tgt)
# only keep the ones within range
new_targets = set(
t for t in new_targets
if math.hypot(SOURCE[0]-t[0], SOURCE[1]-t[1]) <= DISTANCE)
# subtract the ones we already have
new_targets -= all_targets
new_level_targets |= new_targets
if not new_level_targets:
break
# add the new targets
all_targets |= new_level_targets
last_targets = new_level_targets # need these for the next iteration
return all_targets
DISTANCE = 5
all_targets = get_targets(start_point=TARGET, distance=DISTANCE)
print(all_targets)
all_targets is now the set that contains all the reachable points.
(none of that has been thouroughly tested...)
small update for your counter example:
def ray_length(point_list):
d = sum((math.hypot(start[0]-end[0], start[1]-end[1])
for start, end in zip(point_list, point_list[1:])))
return d
d = ray_length(point_list=((1,1),(2.5,2),(3,1.67),(2,1)))
print(d) # -> 3.605560890844135
d = ray_length(point_list=((1,1),(4,3)))
print(d) # -> 3.605551275463989