Исчезающие внутренние граничные условия в модели решетки Больцмана - PullRequest
1 голос
/ 28 октября 2019

Я использую Electron с TypeScript для создания прототипа некоторого кода симуляции жидкости, используя алгоритм Lattice Boltzmann, который в конечном итоге войдет в игру. До сих пор я использовал статические граничные условия (при симуляционных расчетах, происходящих только внутри сетки, а значения для граничных ячеек оставались фиксированными), в этом режиме все работает нормально. В частности, я могу наложить внутренние граничные условия (например, обеспечить, чтобы определенная плотность жидкости всегда выходила из определенного узла решетки на каждом кадре, чтобы имитировать шланг / сопло ракеты / что угодно), просто вручную установив значения ячеек между каждымшаг моделирования.

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

Полный код доступен на https://github.com/gliese1337/balloon-prototype/tree/deopt, но я ожидаю, что соответствующие части выглядят следующим образом:

    class LatticeBoltzmann {        
      private streamed: Float32Array; // microscopic densities along each lattice direction
      private collided: Float32Array;
      public rho: Float32Array;    // macroscopic density; cached for rendering

      ...

      public stream(barriers: boolean[]) {
        const { xdim, ydim, collided, streamed } = this;
        const index = (x: number, y: number) => (x%xdim)+(y%ydim)*xdim;
        const cIndex = (x: number, y: number, s: -1|1, j: number) =>
                          9*(((x+s*cxs[j])%xdim)+((y+s*cys[j])%ydim)*xdim)+j;

        // Move particles along their directions of motion:
        for (let y=1; y<ydim-1; y++) {
          for (let x=1; x<xdim-1; x++) {
            const i = index(x, y);
            const i9 = i*9;
            for (let j=0;j<9;j++) {
              streamed[i9 + j] = collided[cIndex(x, y, -1, j)];
            }
          }
        }

        // Handle bounce-back from barriers
        for (let y=0; y<ydim; y++) {
          for (let x=0; x<xdim; x++) {
            const i = index(x, y);
            const i9 = i*9;
            if (barriers[i]) {
              for (let j=1;j<9;j++) {
                streamed[cIndex(x, y, 1, j)] = collided[i9 + opp[j]];
              }
            }
          }
        }
      }

      // Set all densities in a cell to their equilibrium values for a given velocity and density:
      public setEquilibrium(x: number, y: number, ux: number, uy: number, rho: number) {
        const { xdim, streamed } = this;
        const i = x + y*xdim;
        this.rho[i] = rho;

        const i9 = i*9;
        const u2 =  1 - 1.5 * (ux * ux + uy * uy);
        for (let j = 0; j < 9; j++) {
          const dir = cxs[j]*ux + cys[j]*uy;
          streamed[i9+j] =  weights[j] * rho * (u2 + 3 * dir + 4.5 * dir * dir);
        }
      }
    }

Данные решетки хранятся в двух плоских массивах, collided, который содержит конечные состояния после шага столкновения и служит входом дляэтап потоковой передачи и streamed, который содержит конечные состояния после этапа потоковой передачи и служит в качестве входных данных для следующего этапа столкновения. 9 векторных компонентов для решетки D2Q9 хранятся в смежных блоках, которые затем группируются в строки. Обратите внимание, что я уже использую операции мода для вычисления индексов массива по координатам решетки;это совершенно не имеет значения, если расчеты моделирования распространяются только на внутреннюю часть решетки, но при этом периодические границы должны быть готовы к работе, как только циклы for (let y=1; y<ydim-1; y++) и for (let x=1; x<xdim-1; x++) изменят свои границы на for (let y=0; y<ydim; y++)и for (let x=0; x<xdim; x++) соответственно. И действительно, именно с этим конкретным 6-символьным изменением у меня возникают проблемы.

Метод setEquilibrium используется для наложения граничных условий. В коде драйвера он в настоящее время вызывается так, один раз за кадр:

    // Make fluid flow in from the left edge and out through the right edge
    function setBoundaries(LB: LatticeBoltzmann, ux: number) {
        for (let y=0; y<ydim; y++) {
            LB.setEquilibrium(0, y, ux, 0, 1);
            LB.setEquilibrium(xdim-1, y, ux, 0, 1);
        }
    }

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

Итак ... кто-нибудь знает, что я могу делать неправильно?

1 Ответ

0 голосов
/ 29 октября 2019

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

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

Между прочим, возможность охвата всей решетки без необходимости обрабатывать края специально позволяет сворачивать вложенные циклы в одно линейное сканирование по всей решетке, что устраняет необходимостьфункция вычисления первичного индекса и чрезвычайно упрощает функцию индекса смещения потока при столкновении, cIndex, которая теперь выглядит как const cIndex = (i: number, s: -1|1, j: number) => 9*((i+s*(cxs[j]+cys[j]*xdim)+max)%max)+j;, требуя только один модуль вместо одного на измерение. Результатом этой цепочки упрощений является значительное ускорение кода с соответствующей улучшенной частотой кадров.

...