Перед тем, как приступить к написанию кода, я хотел бы рассказать о математической библиотеке — Eigen. Это зрелая, надежная и эффективная библиотека, которая обладает очень чистым и выразительным API. В ней есть множество compile-time оптимизаций, библиотека способна выполнять явную векторизацию и поддерживает SSE 2/3/4, ARM и NEON инструкции. Как по мне, эта библиотека замечательна хотя бы тем, что позволяет реализовывать сложные математические вычисления кратким и выразительным образом.
Я хотел бы сделать краткое описание части API Eigen, которое мы будем использовать далее. Те читатели, которые знакомы с этой библиотекой, могут пропустить этот раздел.
Есть два типа матриц: плотная и разреженные. Мы будем использовать оба типа. В случае с плотной матрицей, все элементы находятся в памяти, в порядке следования индексов, поддерживаются как column-major (дефолтный) так и raw-major размещение. Этот тип матриц хорош для относительно небольших матриц или матриц с небольшим количеством нулевых элементов. Разреженные матрицы хороши для хранения больших матриц с небольшим количеством ненулевых элементов. Мы будем использовать разреженную матрицу для глобальной матрицы жесткости.
Плотные матрицы
Для использования плотных матриц нам нужно будет включить заголовок . Есть два основных типа плотных матриц: с фиксированным и с динамическим размером. Матрица с фиксированным размером, это такая матрица, размер которой известен во время компиляции. В случае с матрицей динамического размера, ее размер может быть задан на этапе выполнения кода, более того, размер динамической матрицы можно даже изменять налету. Конечно, нужно отдавать предпочтение матрицам с фиксированным размером везде, где это возможно. Память под динамические матрицы выделяется в куче, также неизвестный размер матрицы ограничивает оптимизации, которые компилятор может сделать. Фиксированные матрицы могут быть выделены в стеке, компилятор может развернуть циклы и многое другое. Стоит отметить, что также возможен смешанный тип, когда у матрицы фиксируется количество строк, но количество столбцов динамично, или наоборот.
Матрицы фиксированного размера могут быть определены следующим образом:
Eigen::Matrix m;
Где m — float матрица c фиксированный размеров 3 × 3. Также можно использовать следующие предопределенные типы:
Есть немного больше предопределенных типов, эти являются основными. Цифра обозначает размерность квадратной матрицы и буква обозначает тип элемента матрицы: i — целое число, f — число с плавающей точкой одинарной точности, d — число с плавающей точкой двойной точности.
Для векторов нету отдельного типа, вектора такие-же матрицы. Вектор-столбец (иногда в литературе встречаются векторы-строки, приходится уточнять), можно объявить следующим образом:
Eigen::Matrix v;
Или можно использовать один из предопределенных типов (обозначения те же, что и для матриц):
Для более подробной информации лучше обратиться к документации.
Разреженные матрицы
Разреженная матрица является немного более сложным, типом матрицы. Основная идея не хранить всю матрицу в памяти как есть, а хранить только ненулевые элементы. Довольно часто в практических задачах встречаются большие матрицы с малым количеством ненулевых элементов. При сборке глобальной матрицы жесткости в МКЭ модели, количество ненулевых элементов может быть меньше, чем 0,1% от общего числа элементов.
Для современных пакетов МКЭ не проблема решить задачу с сотней тысяч узлов, причем на вполне обычном современном PC. Если мы попытаемся выделить место для хранения глобальной матрицы жесткости, мы столкнемся со следующей проблемой:
Размер памяти, необходимый для хранения матрицы огромный! Разреженная матрица потребуется в 10000 раз меньшее количество памяти.
Разреженные матрицы используют память более эффективно, так как хранят только ненулевые элементы. Разреженные матрицы могут быть представлены двумя способами: сжатым и несжатым. В несжатом виде легко добавить или удалить элемент из матрицы, но такой вид представления не является оптимальным с точки зрения использования памяти. Eigen, при работе с разреженной матрицей, использует своеобразное сжатое представление, несколько более оптимизированное для динамического добавления/удаления элементов. Eigen также умеет конвертировать такую матрицу в сжатый вид, более того он это делает прозрачно, делать это в явном виде не нужно. Большинство алгоритмов требуют сжатый вид матрицы, и применение любого из этих алгоритмов автоматически преобразует матрицу в сжатый вид. И наоборот, добавление/удаление элемента автоматически конвертирует матрицу в оптимизированное представление.
Как мы можем задать матрицу? Хороший способ это сделать — использовать так называемые Triplets. Это структура данных, причем это шаблонный класс, который представляет собой один элемент матрицы в комбинации с индексами, определяющими его положение в матрице. Мы можем задать разреженную матрицу непосредственно из вектора triplets.
Например, мы имеем следующую разреженную матрицу:
0 3 0 0 0
22 0 0 0 0
0 5 0 1 0
0 0 0 0 0
0 0 14 0 8
Первое, что мы должны сделать, это включить соответствующий заголовок библиотеки Eigen: . Затем, мы объявляем пустую разреженную матрицу размерности 5х5. Далее, мы определяем вектор triplets и заполняем его значениями. Конструктор triplet'а принимает следующие аргументы: i-индекс, j-индекс, значение.
Для хранения данных, которые мы собираемся читать из входного файла, а также промежуточных данных, мы должны объявить свои структуры. Простейшая структура данных описывающая конечный элемент показана ниже. Она состоит из массива трех элементов, которые хранят индексы узлов образующих конечный элемент, а также матрицы В. Данная матрица называемый градиентной матрицей и мы вернемся к ней позже. О методе CalculateStiffnessMatrix также будет рассказано далее.
Еще одна простая структура которая нам понадобится — это структура для описания закреплений. Это простая структура состоящая из перечисляемого типа, который определяет тип ограничения, и индекса узла на который накладывается ограничение.
struct Constraint
{
enum Type
{
UX = 1 << 0,
UY = 1 << 1,
UXY = UX | UY
};
int node;
Type type;
};
Чтобы не усложнять, давайте определим некоторые глобальные переменные. Создавать какие-либо глобальные объекты — это всегда плохая идея, но для этого примера вполне допустимо. Нам понадобятся следующие глобальные переменные:
Количество узлов
Вектор с х-координатой узлов
Вектор с y-координатой узлов
Вектор элементов
Вектор закреплений
Вектор нагрузок
В коде, мы определим их следующим образом:
int nodesCount;
int noadsCount;
Eigen::VectorXf nodesX;
Eigen::VectorXf nodesY;
Eigen::VectorXf loads;
std::vector< Element > elements;
std::vector< Constraint > constraints;
Чтение входного файла
Перед чем как что-то читать, мы должны знать откуда читать и куда писать вывод. В начале main функции проверим количество входных аргументов, обращаю внимание, что первый аргумент всегда путь к исполняемому файлу. Таким образом, у нас должно быть три аргумента, пусть второй будет путь к входному файлу, а третий — путь к файлу с выводом. Для работы с вводом/выводом, для конкретного случая удобнее всего использовать файловые потоки из стандартной библиотеки.
Первое, что мы делаем — создаем потоки для ввода/вывода: