В части первой обсуждались алгоритмы вычисления первой производной дискретной функции (функции, заданной массивом {аргумент: значение}, или массивом значений в узлах на равномерной сетке). Были протестированы функции Python numpy.diff и numpy.gradient (других в Python так и не нашлось), и был предложен алгоритм improved, который, как выяснилось, работает хорошо, но не всегда. При этом проблемы возникают на концах отрезка, на котором определена дифференцируемая функция. Во второй части поработаем с методом конечных элементов (МКЭ, Finite Elements Method), и попробуем построить алгоритм вычисления первой производной, наилучший по точности, и, по возможности, без “слабых мест”.
Во избежание разногласий (а они были в комментариях к части первой) еще разок об объекте исследования. В части первой было сделано одно не совсем корректное допущение. Все исследуемые массивы "визуально" были представлены с точностью до 3-х знаков после запятой. Однако в расчетах использовались точные значения, сгенерированные функциями numpy.exp(x) или x**3. Точность, естественно, была ограничена ошибками машинного округления. Поскольку в этой части повествования мы действуем более аккуратно, то рассматриваться будут дискретные функции, заданные именно массивами с точностью до 4-го знака после запятой. Например, таким:
У этой дискретной функции есть прекрасное свойство - ее первая производная очень похожа на ее саму. Это свойство будет использовано для иллюстрации точности решений разными способами. А о том, что массив сгенерирован экспонентой, забываем и запрещаем в коде использовать функцию numpy.exp(x).
Кроме массива (1) будут исследованы еще несколько других массивов. На них можно посмотреть здесь:
Массивы (дискретные функции) для исследования
Сгенерировано кубической параболой на отрезке :
Первая производная для оценки точности результатов:
Сгенерировано функцией на отрезке :
Первая производная для оценки точности результатов:
Про дискретную функцию (1) мы также знаем, что эта функция задана на отрезке , разделенном на части с равномерным шагом h=0.1. Алгоритм numpy.diff мы из рассмотрения исключим, как “наихудший” из-за несохранения размерности массива данных, и соревнование будет проводиться между остальными двумя ( numpy.gradient и improved), а также новыми алгоритмами, построенными на МКЭ.
Немного теории. Нудновато, но полезно
Я поначалу хотел убрать теоретическую часть в скрытый текст, но в результате получилось совсем немного математики, и, скорее, определение используемых терминов, к которым далее по тексту приходится часто возвращаться.
В рассматриваемом случае МКЭ фактически означает аппроксимацию искомой функции (а также ее первой и даже второй (!!!) производных) кусочно-линейными функциями с последующим решением системы линейных уравнений относительно параметров аппроксимации. Поэтому, если теория, коротко изложенная в этом разделе, покажется не очень простой для восприятия, этот раздел можно пропустить. Общая логика изложения должна быть понятна.
В описании МКЭ присутствует своего рода дуализм. Часто встречающаяся формулировка начинается с разбиения исследуемой области на мелкие части - собственно конечные элементы. В одномерном случае это отрезки, в двумерном - треугольники или 4-угольники, в трехмерном - тетраэдры и т.д. “Внутри” каждого элемента функция представляется достаточно простой, например, линейной. И далее строится система линейных уравнений для вычисления значений функций в узлах, минимизирующих некоторый функционал типа потенциальной энергии или функции ошибок. Мы пойдем другим, более формализованным путем, который (для начала, в одномерном случае) выглядит следующим образом:
Пусть на отрезке задано множество функций , достаточно "хороших" для наших задач - не обязательно непрерывных, но хотя бы квадратично интегрируемых. Для всех таких функций определяется скалярное произведение, например:
, которое определяет меру (модуль функции) :
Кроме этого, на этом же отрезке задается конечный (что, вообще говоря, не обязательно) набор базисных функций (базис):
Изначальная задача - найти набор констант
таких, что линейная комбинация базисных функций с коэффициентами “наилучшим образом” аппроксимирует исследуемую функцию , т.е. обеспечивается минимум модуля их разности ( он же - функция ошибки, если придерживаться терминологии ML):
Для этого необходимо равенство нулю всех частных производных
Это и есть система n+1 линейных уравнений для нахождения n+1 неизвестных величин . В простейшей версии МКЭ для одномерного случая в качестве базисного набора функций выбираются “функции-крышки” - кусочно-линейные функции , такие, что
В этом случае коэффициенты суть не что иное как приближенные значения функции в узлах сетки , что можно обозначить как , а функция аппроксимируется конечно-линейной функцией - линейной комбинацией базисных функций:
Важное замечание, в том числе, по терминологии
В дальнейшем изложении кусочно-линейная функция иногда будет называться вектором . Этот вектор состоит из коэффициентов разложения функции по базису .
В общем случае, коэффициенты разложения , вообще говоря, не совпадают со значениями функции в узлах . Они близки к значениям функции в узлах только потому, что функции базиса выбраны специальным образом - "функций-крышек". Однако, если функция - кусочно-линейная и является линейной комбинацией "функций-крышек", то такое совпадение, очевидно, имеет место, и в этом случае функция сама является своей же точной аппроксимацией, и представима в виде вектора своих узловых значений, что и имеется в виду.
Опуская выкладки, итоговую систему линейных уравнений (3) можно представить в следующем виде:
где G - квадратная матрица с компонентами :
где i, j = 0, …, n, вектор - есть вектор-столбец из неизвестных величин , а вектор -есть вектор-столбец из значений , где i = 0, …, n.
Матрица G как 2-мерный массив скалярных произведений базисных функций часто встречается в различных разделах математики типа дифференциальной геометрии или МДТТ, и имеет много красивых названий - фундаментальный тензор, метрический тензор, матрица жесткости и т.п. Соответственно, решение системы (4) можно представить в виде:
, где - вектор-столбец из значений , и вектор (эквивалентный набору векторов ) называется “сопряженным базисом” и находится по формуле:
Рассмотренный выше метод аппроксимации функции называется, соответственно, методом сопряженных аппроксимаций или методом наилучших аппроксимаций. “Наилучшесть” понимается, естественно, в терминах меры или другой меры, выбранной для задания скалярного произведения функций.
На практике матрицу G обычно явно не обращают, поскольку это очень затратная операция. Полезное свойство матрицы G в нашем (одномерном) случае - трехдиагональность, т.е. все ее ненулевые элементы расположены на главной диагонали (i = j) и на двух соседних диагоналях (| i - j | = 1). При этом система уравнений (4) решается прогонкой, с количеством операций O(n).
Функции-компоненты сопряженного базиса также обычно не вычисляют, однако ниже мы впрямую встретимся с ними, что лично мне кажется достаточно эстетичным.
МКЭ способ 1 ("лобовой")
Итак, начинаем применять теорию на практике. В прошлом дискретная, а теперь кусочно-линейная функция задана массивом значений (1) на отрезке с шагом h = 0.1, т.е. набором значений . При этом:
где - элементы массива (1), а - "функции-крышки". Необходимо найти ее первую производную, или, если точнее, наилучшую аппроксимацию первой производной, “натянутую” на базис .
Важный комментарий для понимания алгоритма
Производная кусочно-линейной функции, если ее считать как обычно, будет кусочно–постоянной и не непрерывной функцией с "разрывами" в узлах , в которых производная не определена. В МКЭ мы ищем ее наилучшую (в смысле меры ) кусочно-линейную аппроксимацию, что можем сделать, вообще говоря, для функции любого вида.
Обозначим первую производную функции как , и будем искать ее аппроксимацию на множестве линейных комбинаций функций-крышек, т е. в виде
, где значения сразу дадут нам искомые значение производной в узлах. Функция ошибки принимает вид:
Раскрываем квадратные скобки под интегралом, “выкидываем” первое слагаемое, поскольку оно не зависит от , и при дифференцировании все равно пропадет, затем ко второму слагаемому применяем интегрирование по частям, после этого система уравнений (3) (про равенство нулю всех частных производных по принимает вид:
где и - суть первый и последний элементы массива (1), соответственно, а - вектор из скалярных произведений первых производных базисных функций и исходной функции . Поскольку базисные функции кусочно линейны, их производные будут кусочно-постоянны, и легко интегрируются.
Конкретный вид матрицы жесткости G и правой части уравнения (7) здесь:
Вектор-столбец в правой части уравнения (7):
Как уже говорилось выше, решаем систему прогонкой, поскольку матрица жесткости - трехдиагональная. Результаты расчета по новому алгоритму FEM представлены на рисунках.
В середине отрезка у всех 3-х алгоритмов точность примерно одинакова, а проблемы, как обычно, на концах. При этом FEM устойчиво показывает результаты лучше, чем numpy.gradient, но хуже, чем improved.
Алгоритм FEM был также протестирован на дискретном аналоге кубической параболы . Вблизи нуля ситуация поменялась в лучшую для FEM сторону:
На левом конце отрезка FEM практически точно ложится на тестовое решение. Вблизи правого конца отрезка "расклад сил" похож на Рис.3.
Подводя итог этого раздела, можно заключить, что "лобовой" метод расчета первой производной с использованием МКЭ ( в его простейшей версии, с "функциями-крышками") устойчиво лучше, чем метод numpy.gradient, но не дает существенного преимущества по сравнению с алгоритмом improved. Возможно ли все-таки на основе МКЭ построить что-то получше? Оказывается, да.
МКЭ способ 2 ("продвинутый")
В основе этого алгоритма лежит, на первый взгляд, парадоксальная идея - чтобы точнее посчитать первую производную функции, надо сразу считать вторую производную. Аналогично выражению (6) представим функцию ошибки для второй производной функции в виде:
где - вторая производная функции , которая, в свою очередь, представлена выражением (5). Казалось бы, вторая производная кусочно-линейной функции - тождественный (кроме точек разрыва) ноль. Ан нет, все не так просто. Повторяем процедуру так же, как в прошлом разделе. Раскрываем квадратные скобки под интегралом, “выкидываем” первое слагаемое, поскольку оно не зависит от , и при дифференцировании все равно пропадет, ко второму слагаемому применяем интегрирование по частям, после этого система уравнений (3) (про равенство нулю всех частных производных функции ошибки по переменным принимает вид (аналогично уравнению (7)):
где - первая производная функции , т.е. кусочно-постоянная функция, которая легко интегрируется в произведении с производными базисных функций - тоже кусочно-постоянными функциями, а и - значения первой производной на левом и правом концах отрезка , соответственно. При всем этом -
значения и заранее неизвестны и пока непонятно, откуда их взять.
Какая при этом получается система линейных уравнений? Матрица жесткости G остается такой же, как в предыдущем разделе (см. спрятанный текст выше), а вектор в правой части уравнения (9) (обозначим его ) заслуживает подробного рассмотрения. Его можно представить в виде трех слагаемых:
Подробный вид слагаемых-векторов можно посмотреть здесь:
А значит, решение системы системы уравнений (9), представленной в виде:
в свою очередь, представимо в виде:
где - решения 3-х систем линейных уравнений с одной и той же матрицей жесткости G и векторами в правой части , соответственно. Эти три вектора-решения являются представлениями кусочно-линейных функций, “разложенных” по базису , причем компоненты каждого вектора являются коэффициентами при базисных "функциях-крышках" в этом разложении. Вектора ( т.е. функции) для нашего исследуемого массива (1) представлены на рис.5.
Зеленый и красный графики почти сливаются на левом конце отрезка, поскольку разница между ними порядка 1, а масштаб рисунка по вертикали значительно больше (значение в нуле порядка 40). Заметим, что вектора и - не что иное как представление двух функций из сопряженного к базиса, а именно, функций и . Вот мы и встретились с сопряженным базисом, как было обещано в начале публикации.
Задача нахождения вектора из выражения (10) поначалу выглядит достаточно сложной. Необходимо провести оптимизацию по двум параметрам и , причем цель оптимизации (типа минимизации какой-то новой функции ошибки) пока даже не сформулирована. Но если внимательно посмотреть на Рис.5, то можно заметить, что все три вектора из (10) имеют ярко выраженной "пилообразный" характер. Поэтому сформулируем:
Цель оптимизации - вычисление вектора , соответствующего "наименее пилообразной" функции, т.е. наиболее гладкой аппроксимации
На рис.5 хорошо видно, что для обоих участвующих в процессе сопряженных вектора и вся их пилообразность “сосредоточена” вблизи концов отрезка - вблизи левого конца отрезка для вектора (функции) с номером 0 (зеленый цвет) и вблизи правого конца отрезка для вектора (функции) с номером n (синий цвет). Сформулируем две гипотезы:
Гипотеза 1. Оптимизацию выражения (10) по двум параметрам и можно проводить независимо по каждому параметру отдельно. Первый будет отвечать за “гладкость” искомого вектора ( т.е. функции ) слева, а второй - справа.
Гипотеза 2. В качестве “меры пилообразности” можно взять угол между первым и вторым звеньями ломаной для нахождения оптимального значение параметра и угол между последним и предпоследним звеньями ломаной для нахождения оптимального значения параметра . Вычислять будем косинус угла между звеньями, который, естественно, должен быть максимален в обоих случаях. Оптимизацию (поиск максимума косинуса) можно проводить разными хорошо известными методами, мы на этом останавливаться не будем.
Для оконгчательного решения поставленной задачи осталось по узловым значениям второй производной ( а это - вектор, он же набор узловых значений ) восстановить набор узловых значений первой производной, например, так:
где определено как значение первого параметра оптимизации. Оказалось, что эти две гипотезы прекрасно работают. Посмотрим на результат (solution) и сравним его с альтернативами. Сначала для экспоненты (ой, обещал забыть название функции):
Теперь для кубической параболы (ой, опять вспомнил название):
Похоже, solution по точности превосходит остальные алгоритмы. Причем, как также выяснилось, не только на концах отрезка. Во внутренних точках его точность также выше.
По секрету сообщаю, что сравнил результаты для функции .
Результат здесь:
Пожалуй, на этом пора заканчивать вторую часть изложения, и так довольно объемно получилось. Похоже, вызревает третья часть. Я все-таки не выполнил обещанное коллеге по результатам обсуждения публикации. Держу в запасе еще один алгоритм нахождения первой производной, который движется ближе к интерполяции. Собственно, из него и возникла идея нахождения второй производной как непрерывной кусочно-линейной функции. Но, как часто бывает, тема сама собой увела в сторону модификации на основе МКЭ.
Наверное, в третью часть также добавлю Python-листинг. Пока код существует в Jupiter Notebook и требует доведения до презентабельного вида. Ну и кое-какие мелочи еще требуется доработать.
Спасибо за внимание. Буду благодарен за комменты и отзывы.