Что такое разреженная матрица

от admin

Разреженная матрица

Разреженная матрица — это матрица с преимущественно нулевыми элементами.

Среди специалистов нет единства в определении того, какое именно количество ненулевых элементов делает матрицу разреженной. Разные авторы предлагают различные варианты. Для матрицы порядка n число ненулевых элементов [1] :

  • есть O(n). Такое определение подходит разве что для теоретического анализа асимптотических свойств матричных алгоритмов;
  • в каждой строке не превышает 10 в типичном случае;
  • ограничено n^<1+\gamma>» width=»» height=»» />, где <img decoding=.
  • таково, что для данного алгоритма и вычислительной системы имеет смысл извлекать выгоду из наличия в ней нулей [1] .

По существу, разреженность соответствует системам со слабыми связями. Можно представить линию шариков, соединенных один за другим пружинками — это пример разреженной системы. Для контраста, если эти же шарики будут соединены пружинками каждый с каждым, то такая система будет представлять собой плотную матрицу. Термин разреженности используется также в комбинаторике и таких прикладных областях как сетевой анализ, при определении малой плотности значимых данных или соединений [источник не указан 70 дней] .

Огромные разреженные матрицы часто возникают при решении таких задач, как дифференциальное уравнение в частных производных.

При хранении и преобразовании разреженных матриц в компьютере бывает полезно, а часто и необходимо, использовать специальные алгоритмы и структуры данных, которые учитывают разреженную структуру матрицы. Операции и алгоритмы, применяемые для работы с обычными, плотными матрицами, работают относительно медленно и требуют значительных объёмов памяти, когда применяются к большим разреженным матрицам. Разреженные данные по природе своей легко сжимаются, а учёт разреженности часто [источник не указан 70 дней] приводит к уменьшению требований к компьютерной памяти.

Содержание

Представление

Алгоритмы

Применение

Библиотеки программ

Для вычислений с разреженными матрицами создано несколько библиотек:

  • SparseLib++ (C++) [2]
  • uBLAS (C++, в составе Boost) [3]
  • SPARSPAK (Fortran) [4]
  • CSparse (C) [5]

Примечания

  1. 12Писсанецки, 1988, Введение
  2. SparseLib++
  3. uBLAS / Boost
  4. Alan George, Esmond Ng A brief description of SPARSPAK Waterloo sparse linear equations package  (англ.) // ACM SIGNUM Newsletter, Volume 19 Issue 4, October 1984. — N.Y, 1984. — С. 17-20. — ISBN 978-1-4503-0245-6. — DOI:10.1145/1057931.1057933
  5. T. A. Davis, Direct Methods for Sparse Linear Systems, SIAM, Philadelphia, September 2006

Литература

  • Reginald P. Tewarson Sparse Matrices. — Academic Press, 1973. — 160 с. — ISBN 0126856508 перевод: Тьюарсон Р. Разреженные матрицы = Sparse Matrices. — М .: Мир, 1977. — 191 с.
  • Писсанецки С. Технология разреженных матриц = Sparse Matrix Technology. — М .: Мир, 1988. — 410 с. — ISBN 5-03-000960-4
  • Джордж А., Лю Дж. Численное решение больших разреженных систем уравнений = Computer Solution of Large Sparse Positive Definite Systems. — М .: Мир, 1984. — 333 с.
  • Найти и оформить в виде сносок ссылки на авторитетные источники, подтверждающие написанное.
  • Комбинаторика
  • Типы матриц

Wikimedia Foundation . 2010 .

Полезное

Смотреть что такое «Разреженная матрица» в других словарях:

разреженная матрица — — [http://www.iks media.ru/glossary/index.html?glossid=2400324] Тематики электросвязь, основные понятия EN sparse matrix … Справочник технического переводчика

РАЗРЕЖЕННАЯ МАТРИЦА — матрица с малым числом ненулевых элементов. Системы линейных уравнений с такими матрицами возникают, в частности, при аппроксимации дифференциальных уравнений конечноразностными или вариационно разностными. Н. С. Бахвалов … Математическая энциклопедия

Матрица линейного оператора — Матрица математический объект, записываемый в виде прямоугольной таблицы чисел (или элементов кольца) и допускающий алгебраические операции (сложение, вычитание, умножение и др.) между ним и другими подобными объектами. Правила выполнения… … Википедия

Квадратная матрица — Матрица математический объект, записываемый в виде прямоугольной таблицы чисел (или элементов кольца) и допускающий алгебраические операции (сложение, вычитание, умножение и др.) между ним и другими подобными объектами. Правила выполнения… … Википедия

Список матриц — Структура матрицы Здесь собраны наиболее важные классы матриц, используемые в математике, науке (в целом) и прикладной науке (в частности). Под матрицей понимается прямоугольный массив чисел … Википедия

Перемножение матриц — Матрица математический объект, записываемый в виде прямоугольной таблицы чисел (или элементов кольца) и допускающий алгебраические операции (сложение, вычитание, умножение и др.) между ним и другими подобными объектами. Правила выполнения… … Википедия

Произведение матриц — Матрица математический объект, записываемый в виде прямоугольной таблицы чисел (или элементов кольца) и допускающий алгебраические операции (сложение, вычитание, умножение и др.) между ним и другими подобными объектами. Правила выполнения… … Википедия

Произведение матрицы на число — Матрица математический объект, записываемый в виде прямоугольной таблицы чисел (или элементов кольца) и допускающий алгебраические операции (сложение, вычитание, умножение и др.) между ним и другими подобными объектами. Правила выполнения… … Википедия

Разница матриц — Матрица математический объект, записываемый в виде прямоугольной таблицы чисел (или элементов кольца) и допускающий алгебраические операции (сложение, вычитание, умножение и др.) между ним и другими подобными объектами. Правила выполнения… … Википедия

Симплекс-метод — Не путать с «симплекс методом»  методом оптимизации произвольной функции. См. Метод Нелдера Мида Симплекс метод  алгоритм решения оптимизационной задачи линейного программирования путём перебора вершин выпуклого многогранника в… … Википедия

Разреженные матрицы

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

Различают два типа разреженных матриц:

матрицы, в которых местоположения элементов со значениями, отличными от фонового, могут быть математически описаны;

матрицы со случайным расположением элементов.

В случае работы с разреженными матрицами вопросы размещения их в памяти реализуются с учетом их типа.

Матрицы с математическим описанием местоположения элементов

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

Элементы, значения которых являются фоновыми, называют нулевыми; а элементы, значения которых отличны от фонового, называют ненулевыми. Но необходимо помнить, что фоновое значение не всегда равно нулю.

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

На практике для работы с разреженной матрицей разрабатываются функции:

для преобразования индексов матрицы в индекс вектора;

для получения значения элемента матрицы из ее упакованного представления по двум индексам (строка, столбец);

для записи значения элемента матрицы в ее упакованное представление по двум индексам.

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

L = ((y-1)*XM+x)/2,

где L — индекс в линейном представлении; x, y – индексы соответственно строки и столбца в двумерном представлении; XM — количество элементов в строке исходной матрицы.

Матрицы со случайным расположением элементов

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

Пусть есть матрица A размерности 57, в которой из 35 элементов только 10 отличны от нуля:

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

Рисунок 6. Последовательное представление разреженных матриц

Доступ к элементу матрицы A с индексами i и j выполняется выборкой индекса i из поля Row, индекса j из поля Colum и значения элемента из поля Value. Следует отметить, что элементы матрицы обязательно запоминаются в порядке возрастания номеров строк для ускорения поиска.

Такое представление матрицы A сокращает используемый объем памяти. Для больших матриц экономия памяти является очень актуальной проблемой.

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

Метод связанных структур переводит статическую структуру матрицы в динамическую. Эта динамическая структура реализована в виде циклических списков.

Для такого представления разреженных матриц требуется следующая структура элемента списка:

Рисунок 7 показывает связанную структуру для разреженной матрицы A.

Рисунок 7. Представление разреженных матриц в виде связанных структур

Циклический список представляет отдельную строку или столбец. Список столбца может содержать общие элементы с одним или более списком строки. Для того чтобы обеспечить использование более эффективных алгоритмов включения и исключения элементов, все списки строк и столбцов имеют головные элементы. Головной элемент каждого списка строки содержит ноль в поле Colum. Аналогично, каждый головной элемент в списке столбца имеет ноль в поле Row. Строка или столбец, содержащие только нулевые элементы, представлены головными вершинами, у которых поле Left или Up указывает само на себя.

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

Стек – это структура данных, в которой новый элемент всегда записывается в ее начало (вершину) и очередной читаемый элемент также всегда выбирается из ее начала. Здесь используется принцип «последним пришел – первым вышел» (LIFO: Last Input – First Output).

Стек можно реализовывать как статическую структуру данных в виде одномерного массива, а можно как динамическую структуру – в виде линейного списка.

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

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

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

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

Рисунок 8. Стек и его организация

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

Описание элементов стека аналогично описанию элементов линейного однонаправленного списка, где DataType является типом элементов стека. Поэтому здесь приводить его не будем.

Основные операции, производимые со стеком:

записать (положить в стек);

прочитать (снять со стека);

проверка пустоты стека.

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

Работа с разреженными наборами данных в pandas и sklearn

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

  • Пользовательский рейтинг рекомендательных систем
  • Пользователь нажимает для рекомендации контента
  • Векторы документов при обработке естественного языка

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

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

Что такое разреженная матрица?

Разреженная матрица — это матрица, в которой большинство элементов равны нулю. Напротив, таблица, в которой большинство элементов ненулевое, называется плотной. Мы определяем разреженность матрицы как количество нулевых элементов, деленное на общее количество элементов. Матрица с разреженностью более 0,5 является разреженной матрицей.

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

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

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

Пример

Рассмотрим следующую таблицу с пользовательскими рейтингами фильмов.

где «Рейтинг» — целевая переменная для задачи классификации нескольких классов.

Теперь представьте, что мы хотим обучить классификатор Factorization Machines. Машины факторизации (FM) — это универсальный предсказатель, который может хорошо работать в задачах с высокой разреженностью, таких как рекомендательные системы. Согласно исходной бумаге нам необходимо преобразовать набор данных в следующий формат:

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

Ниже мы продемонстрируем некоторые способы, которые облегчают преобразование и обработку таких наборов данных в pandas и sklearn.

Набор данных

Мы будем использовать общедоступный набор данных MovieLens 100K, доступный здесь. Учебный файл содержит 100 000 оценок, сделанных 943 пользователями по 1682 пунктам. В рамках этого анализа мы проигнорируем столбец timestamp.

Давайте загрузим данные во фрейм данных pandas.

Быстрое кодирование

Предполагая, что мы хотим преобразовать этот набор данных в формат, показанный в разделе выше, нам нужно быстро кодировать столбцы user_id и item_id. Для преобразования воспользуемся функцией панд get_dummies, которая преобразует категориальные переменные в индикаторные переменные.

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

Теперь давайте применим преобразование и проверим использование памяти преобразованным фреймом данных.

После однократного кодирования мы создали один двоичный столбец для каждого пользователя и один двоичный столбец для каждого элемента. Итак, размер нового фрейма данных составляет 100,000 * 2,626, включая целевой столбец.

Мы видим, что использование памяти преобразованным фреймом данных значительно больше по сравнению с исходным. Это ожидается, поскольку мы увеличили количество столбцов фрейма данных. Тем не менее, большинство элементов в новом фрейме данных — нули.

Совет 1. Используйте разреженные структуры pandas для хранения разреженных данных

Панды разреженные структуры

Pandas предоставляет структуры данных для эффективного хранения разреженных данных. В этих структурах нулевые значения (или любое другое указанное значение) фактически не хранятся в массиве.

Хранение только ненулевых значений и их позиций — распространенный метод хранения разреженных наборов данных.

Мы можем использовать эти структуры, чтобы уменьшить использование памяти нашим набором данных. Вы можете думать об этом как о способе «сжатия» фрейма данных.

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

Если мы проверим dtypes нового фрейма данных, мы увидим, что преобразованные столбцы теперь имеют тип Sparse [uint8, 0]. Это означает, что нулевые значения не сохраняются, а ненулевые значения сохраняются как uint8. Тип dtype ненулевых элементов можно задать при преобразовании в SparseArray.

Кроме того, мы видим, что нам удалось значительно уменьшить использование памяти нашим фреймом данных.

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

Совет 2: используйте разреженную опцию в pandas get_dummies

Можно создать разреженный фрейм данных напрямую, используя параметр sparse в pandas get_dummies. По умолчанию этот параметр имеет значение False. Если True, закодированные столбцы возвращаются как SparseArray. Устанавливая sparse = True, мы создаем разреженный фрейм данных напрямую, без предварительного хранения плотного фрейма данных в памяти.

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

Перейдем к разделению входных и целевых переменных. Мы создадим два набора векторов X, y, используя плотные и разреженные фреймы данных для сравнения.

Разделить X, y
Сплит-тренинг и модельное обучение

Затем мы переходим к sklearn, чтобы выполнить разделение на обучающие тесты для наших наборов данных и обучить модель логистической регрессии. Хотя мы использовали машины факторизации в качестве эталонной модели для создания нашего обучающего набора, здесь мы обучим простую модель логистической регрессии в sklearn только для демонстрации различий в памяти и скорости между плотными и разреженными наборами данных. Советы, которые мы обсудим в этом примере, можно перенести на реализации Python FM, такие как xlearn.

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

Следовательно, уменьшение размера, достигнутое до сих пор с использованием разреженных типов данных, не может быть напрямую перенесено в sklearn. На этом этапе мы можем использовать scipy разреженные форматы и преобразовать наш фрейм данных pandas в scipy разреженную матрицу.

Совет 3: преобразование в scipy разреженную матрицу

Scipy разреженные матрицы

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

В этом примере мы будем использовать форматы lil и csr. В scipy docs вы можете увидеть преимущества и недостатки каждого формата. Для эффективного построения матрицы рекомендуется использовать либо dok_matrix, либо lil_matrix. [«источник»]

Ниже мы определяем функцию для преобразования фрейма данных в scipy разреженную матрицу. Мы начинаем с построения матрицы lil по столбцам, а затем конвертируем ее в csr.

Давайте повторим обучающий тестовый сплит и модельное обучение с матрицей csr.

И train_test_split, и обучение модели были значительно быстрее при использовании X_sparse. Таким образом, делаем вывод, что работа с разреженной матрицей — наиболее эффективный вариант.

Преимущество разреженных матриц будет еще более очевидным в больших наборах данных или наборах данных с большей разреженностью.

Написание МКЭ расчетчика в менее чем 180 строк кода

В наши дни, МКЭ — это наверное самый распространенный метод для решения широкого спектра прикладных инженерных задач. Исторически, он появился из механики, однако впоследствии был применен к всевозможным не механическим задачам.

Сегодня имеется большое разнообразие программных пакетов, таких как ANSYS, Abaqus, Patran, Cosmos, и т.д. Эти программные пакеты позволяют решать задачи строительной механики, механики жидкости, термодинамики, электродинамики и многие другие. Сама реализация метода, как правило считается достаточно сложной и объемной.

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

Картинка для привлечения внимания:

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

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

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

Входные данные

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

На рисунке мы видим девять узлов и восемь элементов. Для описания сетки, нужно два списка:

  • Список узлов, который определяет координаты каждого узла.
  • Список элементов.

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

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

Пусть первая строка будет описание свойств материала. Например, для стали, коэффициент Пуассона ν = 0,3 и модуль Юнга Е = 2000 МПа:

Затем следует строка с количеством узлов и затем сам список узлов:

Затем следует строка с количеством элементов, далее список элементов:

  • 1 — запрещено перемещение в направлении оси x
  • 2 — запрещено перемещение в направлении оси y
  • 3 — запрещено перемещение в обоих, х и у направлениях

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

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

Входной файл целиком:

Eigen — математическая библиотека

Перед тем, как приступить к написанию кода, я хотел бы рассказать о математической библиотеке — Eigen. Это зрелая, надежная и эффективная библиотека, которая обладает очень чистым и выразительным API. В ней есть множество compile-time оптимизаций, библиотека способна выполнять явную векторизацию и поддерживает SSE 2/3/4, ARM и NEON инструкции. Как по мне, эта библиотека замечательна хотя бы тем, что позволяет реализовывать сложные математические вычисления кратким и выразительным образом.
Я хотел бы сделать краткое описание части API Eigen, которое мы будем использовать далее. Те читатели, которые знакомы с этой библиотекой, могут пропустить этот раздел.

Есть два типа матриц: плотная и разреженные. Мы будем использовать оба типа. В случае с плотной матрицей, все элементы находятся в памяти, в порядке следования индексов, поддерживаются как column-major (дефолтный) так и raw-major размещение. Этот тип матриц хорош для относительно небольших матриц или матриц с небольшим количеством нулевых элементов. Разреженные матрицы хороши для хранения больших матриц с небольшим количеством ненулевых элементов. Мы будем использовать разреженную матрицу для глобальной матрицы жесткости.

Плотные матрицы

Для использования плотных матриц нам нужно будет включить заголовок <Eigen/Dense>. Есть два основных типа плотных матриц: с фиксированным и с динамическим размером. Матрица с фиксированным размером, это такая матрица, размер которой известен во время компиляции. В случае с матрицей динамического размера, ее размер может быть задан на этапе выполнения кода, более того, размер динамической матрицы можно даже изменять налету. Конечно, нужно отдавать предпочтение матрицам с фиксированным размером везде, где это возможно. Память под динамические матрицы выделяется в куче, также неизвестный размер матрицы ограничивает оптимизации, которые компилятор может сделать. Фиксированные матрицы могут быть выделены в стеке, компилятор может развернуть циклы и многое другое. Стоит отметить, что также возможен смешанный тип, когда у матрицы фиксируется количество строк, но количество столбцов динамично, или наоборот.

Матрицы фиксированного размера могут быть определены следующим образом:

Где m — float матрица c фиксированный размеров 3 × 3. Также можно использовать следующие предопределенные типы:

Есть немного больше предопределенных типов, эти являются основными. Цифра обозначает размерность квадратной матрицы и буква обозначает тип элемента матрицы: i — целое число, f — число с плавающей точкой одинарной точности, d — число с плавающей точкой двойной точности.

Для векторов нету отдельного типа, вектора такие-же матрицы. Вектор-столбец (иногда в литературе встречаются векторы-строки, приходится уточнять), можно объявить следующим образом:

Или можно использовать один из предопределенных типов (обозначения те же, что и для матриц):

Для быстрого ознакомления, я написал следующий пример:

Для более подробной информации лучше обратиться к документации.

Разреженные матрицы

Разреженная матрица является немного более сложным, типом матрицы. Основная идея не хранить всю матрицу в памяти как есть, а хранить только ненулевые элементы. Довольно часто в практических задачах встречаются большие матрицы с малым количеством ненулевых элементов. При сборке глобальной матрицы жесткости в МКЭ модели, количество ненулевых элементов может быть меньше, чем 0,1% от общего числа элементов.

Для современных пакетов МКЭ не проблема решить задачу с сотней тысяч узлов, причем на вполне обычном современном PC. Если мы попытаемся выделить место для хранения глобальной матрицы жесткости, мы столкнемся со следующей проблемой:

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

Разреженные матрицы используют память более эффективно, так как хранят только ненулевые элементы. Разреженные матрицы могут быть представлены ​​двумя способами: сжатым и несжатым. В несжатом виде легко добавить или удалить элемент из матрицы, но такой вид представления не является оптимальным с точки зрения использования памяти. Eigen, при работе с разреженной матрицей, использует своеобразное сжатое представление, несколько более оптимизированное для динамического добавления/удаления элементов. Eigen также умеет конвертировать такую матрицу в сжатый вид, более того он это делает прозрачно, делать это в явном виде не нужно. Большинство алгоритмов требуют сжатый вид матрицы, и применение любого из этих алгоритмов автоматически преобразует матрицу в сжатый вид. И наоборот, добавление/удаление элемента автоматически конвертирует матрицу в оптимизированное представление.

Как мы можем задать матрицу? Хороший способ это сделать — использовать так называемые Triplets. Это структура данных, причем это шаблонный класс, который представляет собой один элемент матрицы в комбинации с индексами, определяющими его положение в матрице. Мы можем задать разреженную матрицу непосредственно из вектора triplets.

Например, мы имеем следующую разреженную матрицу:

Первое, что мы должны сделать, это включить соответствующий заголовок библиотеки Eigen: <Eigen/Sparse>. Затем, мы объявляем пустую разреженную матрицу размерности 5х5. Далее, мы определяем вектор triplets и заполняем его значениями. Конструктор triplet‘а принимает следующие аргументы: i-индекс, j-индекс, значение.

Вывод будет следующим:

Структуры данных

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

Еще одна простая структура которая нам понадобится — это структура для описания закреплений. Это простая структура состоящая из перечисляемого типа, который определяет тип ограничения, и индекса узла на который накладывается ограничение.

Чтобы не усложнять, давайте определим некоторые глобальные переменные. Создавать какие-либо глобальные объекты — это всегда плохая идея, но для этого примера вполне допустимо. Нам понадобятся следующие глобальные переменные:

  • Количество узлов
  • Вектор с х-координатой узлов
  • Вектор с y-координатой узлов
  • Вектор элементов
  • Вектор закреплений
  • Вектор нагрузок

Чтение входного файла

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

Первое, что мы делаем — создаем потоки для ввода/вывода:

В первой строке входного файла находятся свойства материала, прочитаем их:

Этого достаточно, чтобы построить матрицу упругости изотропного материала для плоско-напряженного состояния, которая определена следующим образом:

Откуда взялись эти выражения? Они легко получаются из закона Гука в общем виде, действительно, мы можем найти выражение для матрицы D из следующих соотношений:

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

При плоско-напряженном состоянии, ничто не мешает телу деформироваться в направлении нормали к его плоскости, поэтому деформация εZ в общем случае не равна нулю. Она не появляется в уравнениях выше, но может быть легко получена из следующего соотношения, учитывая что напряжение σZ равно нулю:

У нас есть все исходные данные чтобы задать матрицу упругости, давайте сделаем это:

Далее, мы читаем список с координатами узлов. Сначала читаем количество узлов, затем задаем размер динамических векторов х и у. Далее, мы просто читать координаты узлов в цикле, строка за строкой.

Затем, мы читаем список элементов. Все то-же самое, читаем количество элементов, а затем индексы узлов для каждого элемента:

Далее, читаем список закреплений. Все то же самое:

Вы могли заметить static_cast, он нужен чтобы преобразовать int тип, в объявленный нами ранее перечисляемый тип для закреплений.

Наконец, нужно прочитать список нагрузок. Есть одна особенность связанная с нагрузкой, из-за которой мы будем представлять действующие нагрузки в виде вектора размерности двойного количества узлов. Причина, почему мы делаем так, будет объяснена чуть позже. Рисунок ниже, иллюстрирует этот вектор нагрузки:

Таким образом, для каждого узла, у нас есть два элемента в векторе нагрузки, которые представляют х и у компоненты усилия. Таким образом, х-компонента усилия в определенном узле будет храниться в элементе с индексом * node_id + 0, а у-составляющая усилия для этого узла будет храниться в элементе с индексом * node_id + 1.

Сначала зададим размер вектора приложенных усилий вдвое больший, чем количество узлов, затем обнулим вектор. Читаем количество внешних сил и читаем их значения строка за строкой:

Расчет глобальной матрицы жесткости

Мы рассматриваем геометрически линейную систему с бесконечно малыми перемещениями. Более того, мы предполагаем, что деформирование происходит упруго, т.е. деформации являются линейной функцией напряжений (закон Гука). Из основ строительной механики можно показать, что перемещение каждого узла является линейной функцией приложенных сил. Таким образом, мы можем говорить, что у нас есть следующая система линейных уравнений:

Где: К — матрица жесткости; δ — вектор перемещений; R — вектор нагрузок, то есть вектор внешних сил. Эту систему линейных уравнений часто называют ансамблем, так как она представляет суперпозицию матриц жесткости каждого элемента, как это будет показано ниже.

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

Где: ui и vi — u-компонента и v-компонента перемещения i-го узла.

Вектор внешних сил:

Где: Rxi и Ryi — х-компонента и y-компонента внешнего усилия приложенного к i-му узлу.

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

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

Как построить матрицу жесткости? Суть глобальной матрицы жесткости есть суперпозиция матриц жесткости каждого элемента. Если мы рассмотрим один элемент отдельно от остальных, то мы можем определить такую-же матрицу жесткости, но только для данного элемента. Эта матрица определяет соотношения между перемещениями узлов и узловыми силами. Например для 3-узлового элемента:

Где: [k] е — матрица жесткости e-го элемента; [δ] е — вектор перемещений узлов е-го элемента; [F] е — вектор узловых сил е-го элемента; i, j, m — индексы узлов элемента.

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

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

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

Если просуммировать все уравнения для каждого узла, меж-элементные узловые силы уйдут. С правой стороны уравнения останутся только внешние силы — нагрузки. Таким образом, учитывая этот факт, мы можем написать:

Рисунок ниже иллюстрирует вышеприведенное выражение:

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

Где std::vector<Eigen::Triplet<float> > — вектор triplets; CalculateStiffnessMatrix — метод класса элемента, который заполняет вектор triplets.

Как уже упоминалось ранее, мы можем построить глобальную разреженную матрицу прямо из вектора triplets:

Расчет матрицы жесткости отдельных элементов

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

Функции формы

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

Где [N] представляет собой матрицу функций координат (х, у). Эти функции называются функциями формы. Каждую компоненту перемещения, u и v можно интерполировать независимо, тогда уравнение выше, можно переписать в следующем виде:

Или можно записать в раздельной форме:


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

Если значения f в узлах известны, то мы можем задать систему из трех уравнений:

Или в матричной форме:

Из этой системы уравнений, мы можем найти неизвестный вектор коэффициентов для интерполирующего выражения:

Наконец, получим интерполяционное выражение:

Возвращаясь к перемещениям, мы можем сказать, что:

Тогда, функции формы будут иметь вид:

Деформации и градиентная матрица

Зная поле перемещений, мы можем найти поле деформаций, по соотношениям из теории упругости:

Теперь мы можем заменить u и v, на функции формы:

Или мы можем написать то же самое в комбинированной форме:

Матрица с правой стороны, обычно обозначается как матрица В, так называемая градиентная матрица.

Чтобы найти матрицу В, мы должны найти все частные производные функций формы:

В случае с линейным элементом, мы видим, что частные производные функций формы на самом деле являются константами, что сэкономит нам множество усилий, как это будет показано далее. Умножив вектор-строку с константами на обратную матрицу C получим:

Теперь мы можем рассчитать B матрицу. Чтобы построить матрицу C, сначала зададим векторы координат узлов:

Затем, мы можем построить матрицу С из трех строк:

Затем мы вычисляем обратную матрицу С и собираем матрицу B:

Переход к напряжениям

Как мы уже упоминали ранее, соотношение между напряжением и деформацией можно записать с помощью матрицы упругости D:

Таким образом, подставляя выражение выше в соотношение для деформаций, получим:

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

Принцип виртуальной работы

Для того, чтобы перейти к матрице жесткости, мы должны обратиться к виртуальной работе. Скажем, у нас есть некие виртуальные перемещения в узлах элемента: d[δ] e

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

Для этих виртуальных перемещений, виртуальная работа узловых сил будет:

Виртуальная работа внутренних напряжений в единице объема для тех-же виртуальный перемещений:

Подставляя уравнение для напряжений, мы получим:

Из закона сохранения энергии, виртуальная работа внешних сил должна быть равна сумме работы внутренних напряжений по объему элемента:

Так, как это соотношение должно быть справедливо для любого виртуального перемещения, мы можем разделить обе части уравнения на виртуальное перемещение:

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

Для линейного элемента, как мы получили ранее, матрица В является константой. Если свойства материала также постоянны, то интеграл становится тривиальным:

Где A — площадь элемента, t — толщина элемента. Из свойств треугольника, его площадь может быть получена как половина детерминанта матрицы С:

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

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

Метод CalculateStiffnessMatrix целиком:

В конце метода, после вычисления матрицы жесткости, заполняется вектор Triplets. Каждый Triplet создается используя массив индексов узлов элемента, чтобы определить его положение в глобальной матрице жесткости. На всякий случай, подчеркну, что у нас есть два набора индексов, один локальный — для матрицы элемента, другой глобальный — для ансамбля.

Задание закреплений

Полученная система линейных уравнений не может быть решена до тех пор, пока мы не зададим закрепления. Чисто с механической точки зрения, незакрепленная система может совершать любые большие перемещения, а под действием внешних сил и вовсе перейдет в движение. Математически, это приведет к тому, что полученная СЛАУ не является линейно независимой, а следовательно не имеет единственного решения.

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

Скажем, мы имеем следующую систему уравнений:

Чтобы закрепить узел, соответствующий элемент матрицы должен быть установлен в 1, и все элементы в строке и столбце содержащие этот элемент должны быть установлены в ноль. Так же не должно быть никаких внешних сил, действующих на закрепленный узел в направлении в котором действует ограничение. Уравнение с этим узлом, явно даст нулевое смещение для этого узла, а нули в соответствующем столбце исключат это смещение из других уравнений. Скажем, мы хотим закрепить нулевой узел в направлении оси х, и закрепить первый узел в направлении оси у:

Чтобы выполнить эту операцию, сначала определим индексы уравнений, которые мы собираемся исключать из СЛАУ. Чтобы это сделать, мы в цикле пройдемся по списку закреплений и соберем вектор индексов. Затем, перебирая все ненулевые элементы матрицы жесткости вызываем функцию SetConstraints. Ниже приведена функция ApplyConstraints:

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

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

Решение СЛАУ и вывод результата

Библиотека Eigen имеет в наличии различные решатели разреженных линейных уравнений, мы будем использовать SimplicialLDLT, который является быстрым прямым решателем. Для демонстрационных целей, мы будем выводить исходную матрицу жесткости и вектор нагрузки, а затем выводить результат решения — вектор перемещений.

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

Анализ полученного результата

Как мы уже видели раньше, мы можем перейти от перемещений к деформациям а далее к напряжениям:

Однако, мы получаем вектор напряжений, который в теории упругости вообще говоря представляет собой симметричный тензор второго ранга:

Как известно, непосредственно элементы тензора зависят от системы координат, однако само физическое состояние конечно не зависит. Поэтому для анализа лучше перейти как какой либо инвариантной величине, лучше всего к скалярной. Таким инвариантом являются напряжения по Мизесу:

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

Теперь мы в цикле пройдемся по всем элементам, и расчитаем напряжения по Мизесу для каждого элемента:

На этом написание нашего простейшего МКЭ расчетчика можно считать законченным. Посмотрим, какой вывод мы получим для нашей тестовой задачи:

Мы видим, что закрепления мы задали верно, деформации закрепленных узлов в соответствующих направлениях равны нулю. Деформации верхних узлов направлены вверх, в соответствии с растягивающим усилием. Деформации в x-направлении, для правой пары узлов составляют величину 0.0003, что есть 0.3 часть от деформаций верхних узлов в y-направлении, что является эффектом Пуассона. Результат расчета полностью согласуется с ожиданиями, поэтому можно переходить к задачам поинтереснее.

Как видим, всего ничего — 171 строчка кода.

Немного картинок

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

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

Здесь мы видим полосу с отверстием, низ которой закреплен, а к верхней кромке приложено растягивающее усилие:

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

Еще один пример с полосой с отверстием, на этот раз рассмотрена только четверть конструкции в силу симметрии. Левая граница закреплена по оси x, нижняя — по оси y. Максимальное полученное напряжение: 3.05152. Данная величина, несмотря на грубость сетки (а может и благодаря ей), достаточно близка к теоретическому значению — 3.

Та-же задача, но решенная в Abaqus. Как видим максимальное напряжение — 3.052, что полностью совпадает с нашим результатом. Такое точное совпадение в принципе не мудрено, так как практически, для треугольного элемента трудно сделать что-то каким-то своим способом, так чтобы результат отличался. Для элементов более высокого порядка у меня к сожалению такого 100%-го совпадения не получилось, что может быть объяснено разной реализацией численного интегрирования.

Весь исходный код, включая примеры доступен на github’е.

Что осталось за кадром

  • Задание внешних нагрузок. Как было сказано, в исходном файле мы задаем узловые силы. Но к ним еще нужно перейти от внешних нагрузок. В случае сконцентрированной силы тут все тривиально, но в случае приложения давления (как в 2-м и 4-м примере), здесь все не так просто. В случае элементов более высоких порядков, результат даже немного неожиданный (с точки зрения инженерной интуиции).
  • Не коснулся я важных свойств функций формы.
  • Хотелось поговорить о элементах более высоких порядков, на примере изопараметрических элементов.
  • Хотелось так-же показать, как выполнить численное интегрирование для элементов более высоких порядков. Это отдельная, весьма любопытная тема.
  • С минимальными изменениями в коде, можно выполнить динамический анализ, когда учитываются силы инерции и система приходит в движение.
  • Можно было, наряду с рассмотрением равновесия узловых сил, привести более общее обоснование метода, исключающее эти узловые силы вообще.

Я думаю, что многие могут справедливо заметить, что в современных математических пакетах, таких как MATLAB есть инструменты для работы с МКЭ, и в принципе нет необходимости что либо писать на С++. Однако, здесь я все же хотел показать минимальную реализацию, с минимумом зависимостей, т.е. без использования сторонних инструментов. За исключением математической библиотеки конечно, но думаю, что все использованные функции для читателей должны быть предельно прозрачны.

Читать:
Чем отличается join от inner join

Похожие статьи