Документация Engee

Смешанная символьно-числовая теория возмущений

История вопроса

Symbolics.jl — это быстрая и современная система компьютерной алгебры (CAS), написанная на языке программирования Julia. Она является неотъемлемой частью экосистемы SciML, состоящей из решателей дифференциальных уравнений и пакетов научного машинного обучения. Хотя язык Symbolics.jl в первую очередь предназначен для современных научных вычислений (например, автодифференцирования, машинного обучения), он представляет собой мощную систему CAS, которая может быть полезна и для классических научных вычислений. Одним из таких применений является использование теории возмущений для решения алгебраических и дифференциальных уравнений.

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

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

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

В этом руководстве мы хотим показать, как использовать сочетание символьных операций (Symbolics.jl) и численных методов для решения простых задач возмущения.

Решение квиники (уравнения пятого порядка)

Начнем с аналога "hello world!" в задачах теории возмущений, решая уравнения пятого порядка. Нам нужно найти вещественное значение , такое, что . Согласно теореме Абеля, общее уравнение пятого порядка не имеет замкнутого решения. Конечно, мы можем легко решить это уравнение численно, например с помощью метода Ньютона. Используем следующую реализацию метода Ньютона.

using Symbolics, SymbolicUtils

function solve_newton(f, x, x₀; abstol=1e-8, maxiter=50)
    xₙ = Float64(x₀)
    fₙ₊₁ = x - f / Symbolics.derivative(f, x)

    for i = 1:maxiter
        xₙ₊₁ = substitute(fₙ₊₁, Dict(x => xₙ))
        if abs(xₙ₊₁ - xₙ) < abstol
            return xₙ₊₁
        else
            xₙ = xₙ₊₁
        end
    end
    return xₙ₊₁
end
solve_newton (generic function with 1 method)

В этом коде Symbolics.derivative(eq, x) делает именно то, что следует из его названия: вычисляет символьную производную eq (выражение Symbolics.jl) по x (переменной Symbolics.jl). Мы используем Symbolics.substitute(eq, D) для определения формулы обновления, подставляя переменные или подвыражения (определенные в словаре D) в eq. Следует отметить, что substitute является исполнительным компонентом нашего кода и будет использоваться много раз в остальных частях этого руководства. solve_newton написан с учетом простоты и ясности, а не производительности.

Вернемся к уравнению пятого порядка. Мы можем определить переменную Symbolics как @variables x, а затем решить уравнение solve_newton(x^5 + x - 1, x, 1.0) (здесь x₀ = 0 — наше первое предположение). Ответ: 0,7549. Теперь посмотрим, как можно решить ту же задачу с помощью методов возмущения.

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

является решением простого уравнения, поэтому . Подставляем в исходную задачу,

Разложив уравнения, получаем

Это уравнение должно выполняться для каждой степени . Таким образом,

и

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

²

И, наконец,

При решении исходной задачи получаем , по сравнению с 0,7548, рассчитанным численно. Мы можем повысить точность, включив больше членов в разложение . Однако вычисления, хотя и являются простыми, очень быстро становятся запутанными и трудновыполнимыми для проведения вручную. Вот почему система CAS очень полезна для решения задач возмущения.

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

n = 2
@variables ϵ a[1:n]
2-element Vector{Any}:
 ϵ
  a[1:2]

Затем определим

x = 1 + a[1]*ϵ + a[2]*ϵ^2
1 + a[1]*ϵ + a[2]*(ϵ^2)

Следующим шагом будет подстановка x в уравнение задачи

  eq = x^5 + ϵ*x - 1
-1 + (1 + a[1]*ϵ + a[2]*(ϵ^2))*ϵ + (1 + a[1]*ϵ + a[2]*(ϵ^2))^5

Разложенная форма eq имеет вид

expand(eq)
ϵ + 5a[1]*ϵ + a[1]*(ϵ^2) + 5a[2]*(ϵ^2) + 10(a[1]^2)*(ϵ^2) + a[2]*(ϵ^3) + 20a[1]*a[2]*(ϵ^3) + 10(a[1]^3)*(ϵ^3) + 10(a[2]^2)*(ϵ^4) + 30(a[1]^2)*a[2]*(ϵ^4) + 5(a[1]^4)*(ϵ^4) + 30a[1]*(a[2]^2)*(ϵ^5) + 20(a[1]^3)*a[2]*(ϵ^5) + 10(a[2]^3)*(ϵ^6) + (a[1]^5)*(ϵ^5) + 30(a[1]^2)*(a[2]^2)*(ϵ^6) + 5(a[1]^4)*a[2]*(ϵ^6) + 20a[1]*(a[2]^3)*(ϵ^7) + 10(a[1]^3)*(a[2]^2)*(ϵ^7) + 5(a[2]^4)*(ϵ^8) + 10(a[1]^2)*(a[2]^3)*(ϵ^8) + 5a[1]*(a[2]^4)*(ϵ^9) + (a[2]^5)*(ϵ^10)

Нам нужен способ получения показателей различных степеней ϵ. Функция collect_powers(eq, x, ns) возвращает степени переменной x в выражении eq. Аргумент ns представляет собой диапазон степеней.

function collect_powers(eq, x, ns; max_power=100)
    eq = substitute(expand(eq), Dict(x^j => 0 for j=last(ns)+1:max_power))

    eqs = []
    for i in ns
        powers = Dict(x^j => (i==j ? 1 : 0) for j=1:last(ns))
        push!(eqs, substitute(eq, powers))
    end
    eqs
end
collect_powers (generic function with 1 method)

Чтобы вернуть показатели и в eq, мы можем написать

eqs = collect_powers(eq, ϵ, 1:2)
2-element Vector{Any}:
                 1 + 5a[1]
 a[1] + 5a[2] + 10(a[1]^2)

Несколько слов о том, как работает функция collect_powers. Она использует substitute для нахождения показателя заданной степени x, передавая Dict со всеми степенями x, которым задано значение 0, кроме целевой степени, которой задано значение 1. Например, следующее выражение возвращает показатель ϵ^2 в eq,

substitute(expand(eq), Dict(
  ϵ => 0,
  ϵ^2 => 1,
  ϵ^3 => 0,
  ϵ^4 => 0,
  ϵ^5 => 0,
  ϵ^6 => 0,
  ϵ^7 => 0,
  ϵ^8 => 0)
)
a[1] + 5a[2] + 10(a[1]^2)

Вернемся к задаче. При наличии показателей степеней ϵ мы можем задать каждое уравнение в eqs равным 0 (помните, мы перестроили задачу так, что eq равен 0) и решить систему линейных уравнений, чтобы найти числовые значения показателей. В Symbolics.jl есть функция Symbolics.solve_for, которая может решать системы линейных уравнений. Однако присутствие членов более высокого порядка в eqs препятствует корректной работе Symbolics.solve_for(eqs .~ 0, a). Мы можем воспользоваться тем фактом, что система имеет треугольную форму, и начать с решения eqs[1] при a₁, затем подставить это в eqs[2] и решить при a₂ и так далее. Этот каскадный процесс реализуется с помощью функции solve_coef(eqs, ps):

function solve_coef(eqs, ps)
    vals = Dict()

    for i = 1:length(ps)
        eq = substitute(eqs[i], vals)
        vals[ps[i]] = Symbolics.solve_for(eq ~ 0, ps[i])
    end
    vals
end
solve_coef (generic function with 1 method)

Здесь eqs является массивом выражений (предполагается, что они равны 0), а ps — массивом переменных. В результате получаем словарь пар переменная => значение. Применим solve_coef к eqs, чтобы получить численные значения параметров:

solve_coef(eqs, a)
Dict{Any, Any} with 2 entries:
  a[2] => -0.04
  a[1] => -0.2

Наконец, подставим обратно значения a в определение x как функцию от 𝜀. Обратите внимание, что 𝜀 — это число (обычно Float64), а ϵ — символьная переменная.

X = 𝜀 -> 1 + a[1]*𝜀 + a[2]*𝜀^2
#7 (generic function with 1 method)

Таким образом, решением нашей исходной задачи становится X(1), что равно 0,76. Можно использовать большие значения n, чтобы повысить точность оценок.

n x

1

0,8

2

0,76

3

0,752

4

0,752

5

0,7533

6

0,7543

7

0,7548

8

0,7550

Помните, что числовое значение равно 0,7549. Во всех примерах этого и следующего руководства используются две функции — collect_powers и solve_coef(eqs, a).

Решение уравнения Кеплера

Исторически методы возмущений были впервые изобретены для выполнения орбитальных расчетов для Луны и планет. Отдавая дань этой истории, наш второй пример связан с космосом. Наша цель — решить уравнение Кеплера:

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

Как и в первом примере, эту задачу легко решить с помощью метода Ньютона. Например, пусть (эксцентриситет Земли) и . solve_newton(x - e*sin(x) - M, x, M) равен 1,5875 (по сравнению с π/2 = 1,5708). Теперь попробуем решить ту же задачу с помощью методов возмущения (см. функцию test_kepler).

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

С этого момента мы используем вспомогательную функцию def_taylor для определения ряда Тейлора, вызывая ее как x = def_taylor(ϵ, a, 1), где аргументами являются, соответственно, переменная возмущения, которая представляет собой массив показателей (начиная с показателя ), и необязательный постоянный член.

def_taylor(x, ps) = sum([a*x^i for (i,a) in enumerate(ps)])
def_taylor(x, ps, p₀) = p₀ + def_taylor(x, ps)
def_taylor (generic function with 2 methods)

Начнем с определения символьных переменных (предполагая, что n = 3):

n = 3
@variables ϵ M a[1:n]
x = def_taylor(ϵ, a, M)
M + a[1]*ϵ + a[2]*(ϵ^2) + a[3]*(ϵ^3)

Еще больше упрощаем задачу, заменяя sin степенным рядом с помощью вспомогательной функции expand_sin:

expand_sin(x, n) = sum([(isodd(k) ? -1 : 1)*(-x)^(2k-1)/factorial(2k-1) for k=1:n])
expand_sin (generic function with 1 method)

Чтобы протестировать,

expand_sin(0.1, 10) ≈ sin(0.1)
true

Уравнение задачи имеет следующий вид:

eq = x - ϵ * expand_sin(x, n) - M
a[1]*ϵ + a[2]*(ϵ^2) + a[3]*(ϵ^3) - (M + a[1]*ϵ + a[2]*(ϵ^2) + a[3]*(ϵ^3) + (1//6)*((-M - a[1]*ϵ - a[2]*(ϵ^2) - a[3]*(ϵ^3))^3) - (1//120)*((-M - a[1]*ϵ - a[2]*(ϵ^2) - a[3]*(ϵ^3))^5))*ϵ

Действуем так же, как и в первом примере. Собираем показатели степеней ϵ

eqs = collect_powers(eq, ϵ, 1:n)
3-element Vector{Any}:
                                                          -M + a[1] + (1//6)*(M^3) - (1//120)*(M^5)
                                              -a[1] + a[2] + (1//2)*(M^2)*a[1] - (1//24)*(M^4)*a[1]
 -a[2] + a[3] + (1//2)*(M^2)*a[2] + (1//2)*M*(a[1]^2) - (1//24)*(M^4)*a[2] - (1//12)*(M^3)*(a[1]^2)

и затем решаем с a:

vals = solve_coef(eqs, a)
Dict{Any, Any} with 3 entries:
  a[2] => M - (1//6)*(M^3) + (1//120)*(M^5) - (1//2)*(M^2)*(M - (1//6)*(M^3) + …
  a[3] => M - (1//6)*(M^3) + (1//120)*(M^5) - (1//2)*(M^2)*(M - (1//6)*(M^3) + …
  a[1] => M - (1//6)*(M^3) + (1//120)*(M^5)

Наконец, подставляем vals обратно в x:

x′ = substitute(x, vals)
X = (𝜀, 𝑀) -> substitute(x′, Dict(ϵ => 𝜀, M => 𝑀))
X(0.01671, π/2)
1.5875853629172587

Результат — 1,5876, по сравнению с числовым значением 1,5875. Принято задавать порядок X на основе степеней 𝑀, а не 𝜀. Мы можем вычислить этот ряд как collect_powers(sol, M, 0:3). Результат (после очистки вручную) таков:

(1 + 𝜀 + 𝜀^2 + 𝜀^3)*𝑀
- (𝜀 + 4*𝜀^2 + 10*𝜀^3)*𝑀^3/6
+ (𝜀 + 16*𝜀^2 + 91*𝜀^3)*𝑀^5/120

Сравнение формулы с формулой для xD835__xDC38 в статье Википедии об уравнении Кеплера:

Первое отклонение — в показателе .