解决C++中浮点数舍入问题

9

我正在开发一个科学应用程序(对细胞核中染色体移动的模拟),其中染色体被分成小片段,使用4x4旋转矩阵围绕随机轴旋转。

问题在于模拟执行数百亿次旋转,因此浮点舍入误差会堆积并呈指数增长,因此片段随着时间的推移往往会“漂离”并从染色体的其他部分分离。

我在C++中使用双精度。目前软件在CPU上运行,但将被移植到CUDA,并且模拟最多可以持续1个月。

我不知道如何以某种方式重新规范染色体,因为所有片段都链接在一起(您可以将其视为双向链表),但我认为这可能是最好的想法,如果可能的话。

你有什么建议吗? 我感到有点迷茫。

非常感谢您,

H。

编辑: 添加了简化的示例代码。 您可以假设所有矩阵数学均为经典实现。

// Rotate 1000000 times
for (int i = 0; i < 1000000; ++i)
{
    // Pick a random section start
    int istart = rand() % chromosome->length;

    // Pick the end 20 segments further (cyclic)
    int iend = (istart + 20) % chromosome->length;

    // Build rotation axis
    Vector4 axis = chromosome->segments[istart].position - chromosome->segments[iend].position;
    axis.normalize();

    // Build rotation matrix and translation vector
    Matrix4 rotm(axis, rand() / float(RAND_MAX));
    Vector4 oldpos = chromosome->segments[istart].position;

    // Rotate each segment between istart and iend using rotm
    for (int j = (istart + 1) % chromosome->length; j != iend; ++j, j %= chromosome->length)
    {
        chromosome->segments[j].position -= oldpos;
        chromosome->segments[j].position.transform(rotm);
        chromosome->segments[j].position += oldpos;
    }
}

3
数值分析和稳定性是广泛的领域,没有一个正确的答案。如果没有看到一些示例代码,很难给出具体建议。 - Oliver Charlesworth
2
顺便说一句,这听起来像是一个很酷的项目。 - Lightness Races in Orbit
7个回答

9
您需要为系统找到一些约束条件,并努力将其保持在某些合理的范围内。我做了一堆分子碰撞模拟,在这些系统中,总能量是守恒的,因此每一步我都会仔细检查系统的总能量,如果它变化超过某个阈值,那么我就知道我的时间步长选择得不好(太大或太小),然后我会选择一个新的时间步长并重新运行它。这样我就可以实时跟踪系统的情况。
对于这个模拟,我不知道您有哪些守恒量,但如果您有一个,可以尝试保持它不变。记住,使时间步长更小并不总是增加精度,您需要根据精度优化步长大小。我曾经进行数周的CPU时间的数值模拟,守恒量始终在10^8的一部分之内,所以这是可能的,您只需要尝试一下。
另外,正如Tomalak所说,也许尝试始终将系统与起始时间进行比较,而不是与上一步进行比较。因此,不要总是移动染色体,而是将染色体保持在其起始位置,并存储一个转换矩阵,使您能够到达当前位置。当计算新的旋转时,只需修改转换矩阵。这可能看起来很傻,但有时它的效果很好,因为误差平均为0。
例如,假设我有一个粒子位于(x,y),每一步我都计算(dx,dy)并移动粒子。逐步进行的方式是这样的:
t0 (x0,y0)
t1 (x0,y0) + (dx,dy) -> (x1, y1)
t2 (x1,y1) + (dx,dy) -> (x2, y2)
t3 (x2,y2) + (dx,dy) -> (x3, y3)
t4 (x3,30) + (dx,dy) -> (x4, y4)
...

如果您始终引用t0,则可以这样做。
t0 (x0, y0) (0, 0)
t1 (x0, y0) (0, 0) + (dx, dy) -> (x0, y0) (dx1, dy1)
t2 (x0, y0) (dx1, dy1) + (dx, dy) -> (x0, y0) (dx2, dy2)
t3 (x0, y0) (dx2, dy2) + (dx, dy) -> (x0, y0) (dx3, dy3)

所以在任何时间,tn,要获得您的真实位置,您需要执行(x0,y0)+(dxn,dyn)

现在对于像我的例子这样简单的翻译,您可能不会赢得很多。 但是对于旋转,这可以拯救生命。 只需保留一个带有每个染色体关联的欧拉角度的矩阵,并更新该矩阵而不是染色体的实际位置。 至少这样它们不会飘走。


+1 用于检查能量。从确保数值收敛到特定解的角度来看,我建议这样做。(但是,我不建议参考长期模拟的开始时间,因为浮点时间值会失去精度并可能停滞。) - Potatoswatter
节约能源是一个很好的方法,至少可以注意到当您的系统出现问题时。如何纠正能量的增益/损失当然可能具有挑战性。它也不是完美的,因为系统的一部分可能会增加,而另一部分则会减少,相互抵消为0。 - edA-qa mort-ora-y
只是为了澄清,像这样的系统中能量并不守恒,因为它不是封闭的。相反,熵被最大化:每个模拟步骤应该有一个相对较高的减少总能量的概率,然后随机波动将温度恢复到正常水平。 - Potatoswatter
1
我刚试了一下变换矩阵的想法,事实上它更糟糕了,因为向量平移被替换成了增加乘法数量和因此精度问题的转换矩阵。另一方面,能量的想法似乎非常有用。染色体倾向于稳定在最低能级,因此包括一个特殊功能“如果你改变大小,那么你就会获得能量”的功能,应该有助于系统收敛而避免错误。谢谢! - user703016

4

请注意,编写公式时,请确保时间步T的数据不仅仅来自于时间步T-1的浮点数据。尽量确保产生浮点误差的步骤限制在单个时间步骤内。

如果没有更具体的问题需要解决,那么很难提供更多的具体信息。


@Heandel:我从你的代码中看到了问题(并记得曾经因此感到沮丧)。就我目前而言,除了使用一些“bignum”库来最大化浮点类型可用的精度之外,我不太确定你能做什么。不过,我可能会错过一些显而易见的东西;我已经很久没有做图形数学了。 - Lightness Races in Orbit

1

问题描述比较模糊,所以这里提供一些相对模糊的建议。

选项1:

找到一些约束条件,使得(1)它们应该始终保持,(2)如果它们失败,但只是轻微的失败,那么很容易调整系统使其成功,(3)如果它们都保持,则您的模拟不会出现严重问题,(4)当系统开始变得疯狂时,约束条件开始失败,但只是稍微失败。例如,也许染色体相邻位之间的距离应该最多为d,对于某个d,如果一些距离略大于d,那么您可以(例如)沿着染色体从一端走,通过将下一个片段向其前任移动以及所有后继来修复任何太大的距离。或者其他什么方法。

然后经常检查约束条件,以确保在捕获时任何违规行为仍然很小;并且当您发现违规行为时,进行修复。(您应该安排当您修复事情时,“超过满足”约束条件。)

如果检查约束条件“始终”很便宜,那么当然可以这样做。(这样做也可能使您能够更便宜地进行修复,例如,如果这意味着任何违规行为始终很小。)

选项2:

找到一种新的描述系统状态的方式,使问题不可能出现。例如,也许(我怀疑这个)你可以为每对相邻的片段存储一个旋转矩阵,并强制它始终是正交矩阵,然后让片段的位置隐含地由这些旋转矩阵确定。

选项3:

不要将约束条件视为约束条件,而是提供一些小的“恢复力”,以便当某些东西偏离轨道时,它倾向于被拉回到应该的方向。请注意,当没有问题时,恢复力为零或至少非常微不足道,以便它们不会比原始数值误差更严重地扰乱您的结果。


0

我认为这取决于你使用的编译器。

Visual Studio编译器支持/fp开关,它告诉浮点运算的行为

你可以阅读更多相关信息。基本上,/fp:strict是最严格的模式


0

我猜这取决于所需的精度,但您可以使用基于“整数”的浮点数。采用此方法,您使用整数并为小数位数提供自己的偏移量。

例如,对于4个小数点的精度,您将拥有

浮点值 -> 整数值 1.0000 -> 10000 1.0001 -> 10001 0.9999 -> 09999

在执行乘法和除法以及应用精度偏移时必须小心。否则,您可能会很快遇到溢出错误。

1.0001 * 1.0001 变成了 10001 * 10001 / 10000


0
如果我正确地阅读了这段代码,那么在任何时候相邻染色体片段之间的距离都不应该改变。在这种情况下,在主循环之前计算每对相邻点之间的距离,并在主循环之后,根据需要移动每个点以使其与前一个点保持适当的距离。
在主循环期间,您可能需要多次强制执行此约束条件,具体取决于情况。

0

基本上,您需要避免这些(不精确的)矩阵运算中的误差积累,并且在大多数应用程序中有两种主要方法可以实现。

  1. 与其将位置写为经过多次操作的某个初始位置,不如明确地写出N次操作后运算符将是什么。例如,假设您有一个位置x并且正在添加一个值e(您无法准确表示)。比起多次计算x += e;,更好的方法是计算x + EN;其中EN是一种更准确地表示操作N次后发生的情况的方式。您应该考虑是否有某种方式可以更准确地表示许多旋转的动作。
  2. 稍微有些人为的是,将新找到的点投影到从旋转中心预期半径的任何差异上。这将确保它不会偏离(但不一定保证旋转角度准确)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接