Решение вопросов округления с плавающей запятой С++

Я разрабатываю научное приложение (моделирование хромосом, движущихся в ядре клетки). Хромосомы разделены на небольшие фрагменты, которые вращаются вокруг случайной оси с использованием матриц вращения 4x4.

Проблема заключается в том, что симуляция выполняет сотни миллиардов оборотов, поэтому ошибки округления с плавающей запятой складываются и растут экспоненциально, поэтому фрагменты стремятся "отплыть" и отходить от остальной хромосомы по прошествии времени.

Я использую двойную точность с С++. На данный момент мягкие запускаются на CPU, но будут перенесены на CUDA, а симуляции могут длиться максимум 1 месяц.

Я понятия не имею, как я мог как-то перенормировать хромосому, потому что все фрагменты соединены вместе (вы можете видеть это как дважды связанный список), но я думаю, что это была бы лучшая идея, если это возможно.

Есть ли у вас предложения? Я чувствую себя немного потерянным.

Большое спасибо,

Н.

EDIT: Добавлен упрощенный пример кода. Вы можете предположить, что все математические матрицы являются классическими реализациями.

// 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;
    }
}

Ответ 1

Вам нужно найти какое-то ограничение для вашей системы и работать, чтобы сохранить это в некоторых разумных пределах. Я сделал кучу моделирования молекулярных столкновений, и в этих системах полная энергия сохраняется, поэтому каждый шаг я дважды проверяю полную энергию системы, и если она изменяется на некоторый порог, то я знаю, что мой временной шаг был плохо выбран (слишком большой или слишком маленький), и я выбираю новый временной шаг и повторно запускаю его. Таким образом, я могу отслеживать, что происходит с системой в реальном времени.

Для этого моделирования я не знаю, что у вас сохраняется, но если у вас есть, вы можете попытаться сохранить эту константу. Помните, что уменьшение вашего временного шага не всегда повышает точность, вам нужно оптимизировать размер шага с той степенью точности, которую вы имеете. У меня было числовое моделирование, выполняемое в течение недель процессорного времени, и сохраненные количества всегда находились в пределах 1 части в 10 ^ 8, так что это возможно, вам просто нужно поиграть с некоторыми.

Кроме того, как сказал Томалак, возможно, попробуйте всегда ссылаться на свою систему на время начала, а не на предыдущий шаг. Поэтому, вместо того, чтобы всегда перемещать ваши хромосомы, храните хромосомы в своем стартовом месте и храните вместе с ними матрицу преобразования, которая приведет вас к текущему местоположению. Когда вы вычисляете новое вращение, просто измените матрицу преобразования. Это может показаться глупым, но иногда это хорошо работает, потому что ошибки в среднем равны 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)

Теперь для простого перевода, подобного моему примеру, вы вряд ли выиграете. Но для вращения это может быть спасателем жизни. Просто держите матрицу с углами Эйлера, связанными с каждой хромосомой, и обновляйте ее, а не фактическое положение хромосомы. По крайней мере, так они не уплывут.

Ответ 2

Напишите ваши формулы, чтобы данные для timestep T не выводились исключительно из данных с плавающей запятой в timestep T-1. Постарайтесь обеспечить, чтобы выпуск ошибок с плавающей запятой ограничивался одним временным шагом.

Трудно сказать что-то более конкретное здесь без более конкретной проблемы, чтобы решить.

Ответ 3

Описание проблемы довольно расплывчато, поэтому вот несколько довольно расплывчатых предложений.

Вариант 1:

Найдите некоторый набор ограничений, таких, что (1) они должны всегда выполняться, (2) если они терпят неудачу, но только просто, легко настроить систему так, чтобы они выполнялись, (3) если они все удерживают, тогда ваш симуляция не идет плохо сумасшедшим, и (4) когда система начинает сходить с ума, ограничения начинают сбой, но только незначительно. Например, возможно, расстояние между соседними битами хромосомы должно быть не более d, для некоторого d, и если несколько расстояний немного больше d, тогда вы можете (например) пройти по хромосоме с одного конца, исправить любые расстояния, которые слишком велики, перемещая следующий фрагмент к своему предшественнику вместе со всеми его преемниками. Или что-то.

Затем проверьте ограничения достаточно часто, чтобы быть уверенным, что любое нарушение будет по-прежнему небольшим при попадании; и когда вы поймаете нарушение, исправьте ситуацию. (Возможно, вам стоит договориться, что, когда вы исправляете ситуацию, вы "более чем удовлетворяете" ограничениям.)

Если дешево проверять ограничения все время, то, конечно, вы можете это сделать. (Это может также позволить вам сделать исправление более дешево, например, если это означает, что любые нарушения всегда крошечные.)

Вариант 2:

Найдите новый способ описания состояния системы, который делает невозможным возникновение проблемы. Например, возможно (я сомневаюсь в этом) вы можете просто сохранить матрицу вращения для каждой смежной пары фрагментов и заставить ее всегда быть ортогональной матрицей, а затем пусть положения фрагментов будут неявно определяться этими матрицами вращения.

Вариант 3:

Вместо того, чтобы думать о ваших ограничениях как о ограничениях, поставьте небольшие "восстанавливающие силы", чтобы, когда что-то выходит из строя, оно стремится отступить к тому, каким оно должно быть. Позаботьтесь о том, чтобы, когда что-то не так, восстанавливающие силы равны нулю или, по крайней мере, очень незначительны, чтобы они не нарушали ваши результаты более сильно, чем исходные числовые ошибки.

Ответ 4

Я думаю, что это зависит от используемого вами компилятора.

Компилятор Visual Studio поддерживает/fp-переключатель, который сообщает поведение операций с плавающей запятой

вы можете узнать больше об этом. В принципе, /fp: strict - это самый жесткий режим

Ответ 5

Я предполагаю, что это зависит от требуемой точности, но вы можете использовать числа с плавающей запятой, основанные на целых числах. При таком подходе вы используете целое число и предоставляете собственное смещение для количества десятичных знаков.

Например, с точностью до 4 десятичных точек у вас будет

значение float → значение int 1.0000 → 10000 1.0001 → 10001 0.9999 → 09999

Вы должны быть осторожны, когда делаете свое умножение и разделяете и будьте осторожны, применяя прецизионные смещения. В противном случае вы можете быстро получить ошибки переполнения.

1.0001 * 1.0001 становится 10001 * 10001/10000

Ответ 6

Если я правильно прочитал этот код, ни в коем случае не должно быть расстояние между любыми двумя соседними сегментами хромосом, которое должно измениться. В этом случае, прежде чем основной цикл вычислит расстояние между каждой парой соседних точек и после основного цикла, переместите каждую точку, если необходимо, чтобы иметь правильное расстояние от предыдущей точки.

Вам может потребоваться принудительное применение этого ограничения несколько раз во время основного цикла, в зависимости от обстоятельств.

Ответ 7

В принципе, вам нужно избегать накопления ошибок от этих (неточных) матричных операторов, и в большинстве приложений есть два основных способа сделать это.

  • Вместо того, чтобы записывать позицию как некоторая начальная позиция, работающая много раз, вы можете записать, что оператор будет явно после N операций. Например, представьте, что у вас есть позиция x, и вы добавляли значение e (которое вы не могли точно представить). Гораздо лучше, чем вычисление x + = e; большое количество раз было бы вычислять x + EN; где EN - это более точный способ представления того, что происходит с операцией после N раз. Вы должны подумать, есть ли у вас некоторый способ представления действия многих поворотов более точно.
  • Чуть более искусственное - это взять вновь найденную точку и отбросить любые расхождения из ожидаемого радиуса от вашего центра вращения. Это гарантирует, что он не дрейфует (но не обязательно гарантирует точность угла поворота).