Performance 按角度对向量进行排序的最快方法,无需实际计算角度
许多算法(例如)要求按角度对点或向量进行排序(可能从其他点看,即使用差分向量)。这个顺序本质上是循环的,在哪里打破这个循环来计算线性值通常并不重要。但是,只要保持循环顺序,实际角度值也无关紧要。因此,对每个点执行Performance 按角度对向量进行排序的最快方法,无需实际计算角度,performance,sorting,math,geometry,angle,Performance,Sorting,Math,Geometry,Angle,许多算法(例如)要求按角度对点或向量进行排序(可能从其他点看,即使用差分向量)。这个顺序本质上是循环的,在哪里打破这个循环来计算线性值通常并不重要。但是,只要保持循环顺序,实际角度值也无关紧要。因此,对每个点执行atan2调用可能是浪费。有什么更快的方法来计算角度上严格单调的值,就像atan2那样?这些函数显然被一些人称为“伪角度”。我知道一个可能的函数,我将在这里描述 #输入:dx,dy:一个(差)向量的坐标。 #输出:范围[-1..3](或启用注释的[0..4]中的数字) #这个向量相对于x
atan2
调用可能是浪费。有什么更快的方法来计算角度上严格单调的值,就像atan2
那样?这些函数显然被一些人称为“伪角度”。我知道一个可能的函数,我将在这里描述
#输入:dx,dy:一个(差)向量的坐标。
#输出:范围[-1..3](或启用注释的[0..4]中的数字)
#这个向量相对于x轴的角度是单调的。
def伪角度(dx,dy):
ax=abs(dx)
ay=绝对值(dy)
p=dy/(ax+ay)
如果dx<0:p=2-p
#elif dy<0:p=4+p
返回p
那么,这是为什么?需要注意的一点是,缩放所有输入长度不会影响输出。所以向量的长度(dx,dy)是无关的,只有它的方向是重要的。专注于第一象限,我们现在可以假设
dx==1
。然后dy/(1+dy)
从dy==0
的0单调增长到无限dy
(即dx==0
)的1。现在,其他象限也必须处理。如果dy
是负数,那么初始p
也是负数。因此,对于肯定的dx
我们已经有了一个范围-1,我开始研究这个,并意识到规范有点不完整atan2
具有不连续性,因为随着dx和dy的变化,atan2
将在-pi和+pi之间跳跃。下图显示了@MvG建议的两个公式,事实上,与atan2
相比,它们在不同的位置都具有不连续性。(注意:我在第一个公式中加了3,在另一个公式中加了4,这样线就不会在图上重叠)。如果我将atan2
添加到该图中,那么它将是直线y=x。所以在我看来,可能会有各种各样的答案,这取决于人们想把不连续性放在哪里。如果你真的想复制atan2,答案(在这种类型中)是
# Input: dx, dy: coordinates of a (difference) vector.
# Output: a number from the range [-2 .. 2] which is monotonic
# in the angle this vector makes against the x axis.
# and with the same discontinuity as atan2
def pseudoangle(dx, dy):
p = dx/(abs(dx)+abs(dy)) # -1 .. 1 increasing with x
if dy < 0: return p - 1 # -2 .. 0 increasing with x
else: return 1 - p # 0 .. 2 decreasing with x
@MvG说它工作得很好,对我来说也不错:-)
很好。。这是一个返回-Pi的变量,与许多arctan2函数类似
编辑说明:将我的伪代码更改为正确的python。。arg顺序已更改,以便与pythons数学模块atan2()兼容。Edit2需要更多的代码来捕捉dx=0的情况
def pseudoangle( dy , dx ):
""" returns approximation to math.atan2(dy,dx)*2/pi"""
if dx == 0 :
s = cmp(dy,0)
else::
s = cmp(dx*dy,0) # cmp == "sign" in many other languages.
if s == 0 : return 0 # doesnt hurt performance much.but can omit if 0,0 never happens
p = dy/(dx+s*dy)
if dx < 0: return p-2*s
return p
def伪角度(dy,dx):
“”“返回数学近似值。atan2(dy,dx)*2/pi”“”
如果dx==0:
s=cmp(dy,0)
其他::
s=cmp(dx*dy,0)#cmp==许多其他语言中的“符号”。
如果s==0:return 0#对性能影响不大,但如果0,0从未发生,则可以忽略
p=dy/(dx+s*dy)
如果dx<0:返回p-2*s
返回p
在这种形式下,所有角度的最大误差仅为~0.07弧度。
(当然,如果您不关心大小,请省略Pi/2。)
现在来看坏消息——在我的系统上,使用python math.atan2大约快25%
显然,替换一个简单的解释代码比不上编译的内部代码。只需使用一个叉积函数即可。相对于另一段旋转一段的方向将给出一个正数或负数。没有触发功能,也没有除法。快速简单。只需谷歌一下。我有点喜欢三角学,所以我知道将一个角度映射到我们通常拥有的某些值的最佳方法是切线。当然,如果我们想要一个有限的数字,这样就不会有比较{sign(x),y/x}的麻烦,它会变得更加混乱
但是有一个函数将[1,+inf]映射到[1,0],称为逆函数,它允许我们有一个有限的范围来映射角度。切线的逆函数是众所周知的余切,因此x/y(是的,就是这么简单)
一个小插图,显示单位圆上的切线和余切值:
当| x |=| y |时,你会看到值是相同的,你还会看到,如果我们给两个圆上输出值在[-1,1]之间的部分上色,我们会给一个完整的圆上色。要使这个值映射连续且单调,我们可以做两件事:
- 使用余切的对立面使其与切线具有相同的单调性
- 将2添加到-cotan,使tan=1时的值重合
- 将4添加到圆的一半(例如,在x=-y对角线下方),使值与其中一个不连续点相匹配
这就给出了以下分段函数,它是角度的连续单调函数,只有一个不连续(最小值):
注意,这非常接近,具有相同的属性。形式上,pseudoangle(dx,dy)+1%8==Fowler(dx,dy)
就性能而言,它比Fowler的代码要简单得多(imo通常也不那么复杂)。在gcc 6.1.1上使用-O3
编译,上面的函数生成一个包含4个分支的汇编代码,其中两个分支来自dy==0
(一个检查两个操作数是否“无序”),因此,如果dy是NaN
,则另一个检查它们是否相等)
我认为这个版本比其他版本更精确,因为它只使用保留尾数的运算,直到将结果移动到正确的间隔。当| x |>|x |,然后运算| x |+| y |失去一些精度时,这应该特别明显
如图所示,角度-伪角度关系也非常接近线性
<
def pseudoangle( dy , dx ):
""" returns approximation to math.atan2(dy,dx)*2/pi"""
if dx == 0 :
s = cmp(dy,0)
else::
s = cmp(dx*dy,0) # cmp == "sign" in many other languages.
if s == 0 : return 0 # doesnt hurt performance much.but can omit if 0,0 never happens
p = dy/(dx+s*dy)
if dx < 0: return p-2*s
return p
double pseudoangle(double dx, double dy)
{
// 1 for above, 0 for below the diagonal/anti-diagonal
int diag = dx > dy;
int adiag = dx > -dy;
double r = !adiag ? 4 : 0;
if (dy == 0)
return r;
if (diag ^ adiag)
r += 2 - dx / dy;
else
r += dy / dx;
return r;
}
double pseudoangle(double dx, double dy)
{
double s = dx + dy;
double d = dx - dy;
double r = 2 * (1.0 - copysign(1.0, s));
double xor_sign = copysign(1.0, d) * copysign(1.0, s);
r += (1.0 - xor_sign);
r += (s - xor_sign * d) / (d + xor_sign * s);
return r;
}
signed int compare(double x1, double y1, double x2, double y2) {
unsigned int d1 = x1 > y1;
unsigned int d2 = x2 > y2;
unsigned int a1 = x1 > -y1;
unsigned int a2 = x2 > -y2;
// Quotients of both angles.
unsigned int qa = d1 * 2 + a1;
unsigned int qb = d2 * 2 + a2;
if(qa != qb) return((0x6c >> qa * 2 & 6) - (0x6c >> qb * 2 & 6));
d1 ^= a1;
double p = x1 * y2;
double q = x2 * y1;
// Numerator of each remainder, multiplied by denominator of the other.
double na = q * (1 - d1) - p * d1;
double nb = p * (1 - d1) - q * d1;
// Return signum(na - nb)
return((na > nb) - (na < nb));
}
using StableRNGs
using BenchmarkTools
# Definitions
struct V{T}
x::T
y::T
end
function pseudoangle(v)
copysign(1. - v.x/(abs(v.x)+abs(v.y)), v.y)
end
function isangleless(v1, v2)
a1 = abs(v1.x) + abs(v1.y)
a2 = abs(v2.x) + abs(v2.y)
a2*copysign(a1 - v1.x, v1.y) < a1*copysign(a2 - v2.x, v2.y)
end
# Data
rng = StableRNG(2021)
vectors = map(x -> V(x...), zip(rand(rng, 1000), rand(rng, 1000)))
# Comparison
res1 = sort(vectors, by = x -> pseudoangle(x));
res2 = sort(vectors, lt = (x, y) -> isangleless(x, y));
@assert res1 == res2
@btime sort($vectors, by = x -> pseudoangle(x));
# 110.437 μs (3 allocations: 23.70 KiB)
@btime sort($vectors, lt = (x, y) -> isangleless(x, y));
# 65.703 μs (3 allocations: 23.70 KiB)