Перейти к основному содержимому

Сеточная адаптация

В проекте реализована локальная сеточная адаптация по типу иерархических окто-деревьев. Хотя деревьями сетка является только по виду. Фактически расчетная сетка содержит только листовые ячейки, которые не связаны в древовидную структуру.

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

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

Уровень адаптации ячеек на базовой сетке считается равным нулю. Двумрная ячейка может разбиться на 4 дочерних ячейки, трёхмерная ячейка – на восемь дочерних ячеек. Уровень адаптации дочерних ячеек считается на единицу выше родительской. Ячейки, которые имеют одного родителя, могут объединиться снова.

При выполнении адаптации уровни адаптации соседних через грань ячеек могут отличаться максимум на единицу. Таким образом, линейные размеры соседних ячеек отличаются не более чем в два раза. Если уровень ячейки через грань на единицу выше, то грань разбивается на 2 или 4 пограни (в зависимости от размерности). В двумерном случае ячейка может иметь от 4 до 8 граней, а в трёхмерном – от 6 до 24 граней.

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

mesh.set_max_level(5);

По умолчанию максимальный уровень адаптации равен нулю, в этом случае любой вызов адаптации не будет приводить к каким-либо результатам.

Выставление флагов

Для проведения адаптации необходимо выставить в каждой ячейке сетки флаг адаптации. Флаг > 0 означает требование разбить ячейку, флаг = 0 – требование оставить ячейку без изменений, флаг < 0 – требование огрубить (объединить) ячейки.

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

auto chi = mesh.add<double>("chi");

mesh.for_each([](EuCell cell) {
if (cell[chi] < 0.05)
cell.set_flag(-1); // Объединить ячейки
else if (cell[chi] < 0.2)
cell.set_flag(0); // Ничего не делать
else
cell.set_flag(1); // Разбить ячейку
});

mesh.refine(); // Выполнить адаптацию

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

После задания всех флагов вызывается функция mesh.refine(), которая проводит адаптацию сетки. Функцию refine() можно вызывать в произвольном месте программы.

Балансировка флагов адаптации для соблюдения соотношения уровней между соседними ячейками выполняется автоматически внутри функции refine(). При балансировке флаги адаптации могут измениться в большую сторону. Ячейки с флагом > 0 разбиваются всегда, если не достигли максимального уровня адаптации. Ячейки с флагом < 0 могут объединиться только в том случае, если все дочерние ячейки имеют флаг < 0.

Распределение данных

При адаптации на месте старых ячеек появляются новые. В новые ячейки необходимо поместить расчетные данные. Для этого используется структура zephyr::mesh::Distributor. В структуре должны быть заданы две функции:

  • Функция split, которая определяет распределение данных при разбиении ячейки.
  • Функция merge, которая определяет распределение данных при слиянии ячеек.

Есть несколько предопределенных распределителей.

  • Distributor::empty(). Этот распределитель ничего не делает. После создания новых ячеек в них окажутся незвестные данные.
  • Distributor::simple(). При разбиении копирует в дочерние ячейки данные из родительской ячейки. При слиянии ячеек копирует в родительскую ячейку данные из первой дочерней. При использовании в расчетах приводит к существенной асимметрии (из-за некорректной операции слияния), но его можно использовать на первое время.

После задания функций Distributor его можно задать на сетку.

Distributor distr = Distributor::Simple();
mesh.set_distributor(distr);

По умолчанию на сетке используется Distributor::empty().

Создадим более адекватный распределитель. Пусть на сетке задано скалярное поле u. Для операции split будем использовать стандартное распределние из Distributor::simple(), а при слиянии ячеек будем вычислять среднее значение.

Storable<double> u = mesh.add<double>("u");

Distributor distr = Distributor::Simple();
distr.merge = [u](const Children &children, EuCell &parent) {
double sum = 0.0;
for (auto child: children) {
sum += child[u] * child.volume();
}
parent[u] = sum / parent.volume();
};

mesh.set_distributor(distr);

Пойдем дальше. Пусть помимо скалярного поля в ячейках также определен градиент функции. Тогда при разбиении можно использовать перенос величин по Тейлору. Функцию merge оставим как прежде, добавим новую функцию для split.

Storable<double> u = mesh.add<double>("u");
Storable<Vector3d> grad = mesh.add<double>("grad_u");

Distributor distr;
distr.merge = ...

distr.split = [u, grad](const EuCell &parent, Children &children) {
for (auto child: children) {
Vector3d dr = parent.center() - child.center();
child[u] = parent[u] + parent[grad].dot(dr);
}
};

mesh.set_distributor(distr);

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

Важное ограничение: распределители не имеют доступа к данным соседних ячеек.