Начало работы с Symbolics.jl
Создание символьных выражений
Для определения символьных переменных используется макрос @variables
:
using Symbolics
@variables x y
2-element Vector{Num}:
x
y
После определения переменных как символьных с помощью выражений Julia можно генерировать символьные выражения, которые мы называем объектами istree
. Например:
z = x^2 + y
y + x^2
Здесь z
— это дерево выражений для выражения «возвести в квадрат x
и прибавить y
». Чтобы создать массив символьных выражений, просто создайте массив символьных выражений:
A = [x^2 + y 0 2x
0 0 2y
y^2 + x 0 0]
3×3 Matrix{Num}:
y + x^2 0 2x
0 0 2y
x + y^2 0 0
Обратите внимание, что по умолчанию @variables
возвращает объекты Sym
или istree
, заключенные в Num
, чтобы они действовали как подтипы Real
. Любая операция с этими объектами Num
возвратит новый объект Num
с заключенным в него результатом символьных вычислений с базовыми значениями.
Если вы выполняете действия из этого руководства в REPL Julia, A
не будет отображаться с уравнениями LaTeX. Чтобы получить эти уравнения, можно использовать Latexify.jl. Symbolics.jl содержит шаблоны Latexify, поэтому он работает автоматически:
using Latexify
latexify(A)
Обычные функции Julia работают с выражениями Symbolics, поэтому, если нужно создать разреженную версию A
, следует просто вызвать sparse
:
using SparseArrays
spA = sparse(A)
latexify(A)
Таким образом, мы можем использовать обычные функции Julia в качестве генераторов разреженных выражений. Например, здесь мы определим следующее:
function f(u)
[u[1] - u[3], u[1]^2 - u[2], u[3] + u[2]]
end
f([x, y, z]) # Вспомним, что z = x^2 + y
3-element Vector{Num}:
x - y - (x^2)
-y + x^2
2y + x^2
Или мы можем создать переменную массива и использовать ее для трассировки функции:
@variables u[1:4]
f(u)
3-element Vector{Num}:
u[1] - u[3]
-u[2] + u[1]^2
u[2] + u[3]
Производные
В символьной системе часто приходится вычислять производные. В Symbolics.jl производные «лениво» представлены с помощью операций, как и любые другие функции. Чтобы создать дифференциальный оператор, используйте Differential
, как показано далее:
@variables t
D = Differential(t)
(::Differential) (generic function with 3 methods)
Это дифференциальный оператор *
, например Differential(t) * Differential(x)
или Differential(t)^2
. Теперь запишем производную некоторого выражения:
z = t + t^2
D(z)
Differential(t)(t + t^2)
Заметьте, что это еще ничего не вычисляет: D
— это ленивый оператор, так как позволяет символически представить «производную expand_derivatives
:
expand_derivatives(D(z))
1 + 2t
Доступ к переменной, по которой берется производная, осуществляется так:
D.x
t
Кроме того, можно использовать упрощенные функции для анализа функций многих переменных. Например, мы можем вычислить якобиан массива выражений следующим образом:
Symbolics.jacobian([x + x*y, x^2 + y], [x, y])
2×2 Matrix{Num}:
1 + y x
2x 1
и точно так же можно поступать с гессианами, градиентами и определять любые другие производные.
Упрощение и подстановка
Symbolics взаимодействует с SymbolicUtils.jl для упрощения символьных выражений. Для этого нужно просто выполнить команду simplify
:
simplify(2x + 2y)
2(x + y)
Ее можно применить к массивам, используя механизм трансляции Julia:
B = simplify.([t + t^2 + t + t^2 2t + 4t
x + y + y + 2t x^2 - x^2 + y^2])
2×2 Matrix{Num}:
2(t + t^2) 6t
2(t + y) + x y^2
Затем с помощью substitute
можно изменить значения выражения:
simplify.(substitute.(B, (Dict(x => y^2),)))
2×2 Matrix{Num}:
2(t + t^2) 6t
2(t + y) + y^2 y^2
и это можно использовать его для интерактивной оценки выражений без генерации и компиляции функций Julia:
V = substitute.(B, (Dict(x => 2.0, y => 3.0, t => 4.0),))
2×2 Matrix{Num}:
40.0 24.0
16.0 9.0
Где мы можем ссылаться на значения с помощью
Symbolics.value.(V)
2×2 Matrix{Float64}:
40.0 24.0
16.0 9.0
Неинтерактивная разработка
Обратите внимание, что макросы предназначены для высокоуровневого случая, когда вы выполняете символьные вычисления в собственном коде. Чтобы проводить символьные вычисления в чужом коде, например в макросе, можно не выполнять @variables x
, так как может потребоваться, чтобы имя «x» было взято из кода пользователя. Для таких случаев можно использовать оператор интерполяции, чтобы интерполировать значение среды выполнения x
, т. е. @variables $x
. Дополнительные сведения см. в документации по @variables
.
a, b, c = :runtime_symbol_value, :value_b, :value_c
(:runtime_symbol_value, :value_b, :value_c)
vars = @variables t $a $b(t) $c(t)[1:3]
4-element Vector{Any}:
t
runtime_symbol_value
value_b(t)
(value_c(t))[1:3]
(t, a, b, c)
(t, :runtime_symbol_value, :value_b, :value_c)
Можно также использовать variable
и variables
. Дополнительные сведения см. в соответствующей документации.
Чтобы использовать их для генерации нового кода Julia, можно просто преобразовать вывод в Expr
:
Symbolics.toexpr(x + y^2)
:((+)(x, (^)(y, 2)))
Sym
и вызываемые Sym
В определении
@variables t x(t) y(t)
3-element Vector{Num}:
t
x(t)
y(t)
t
имеет тип Sym{Real}
, но имя x
указывает на объект, который представляет Term
x(t)
. Операцией этого выражения является сам объект Sym{FnType{Tuple{Real}, Real}}(:x)
. Тип Sym{FnType{...}}
представляет вызываемый объект. В данном случае это функция, которая принимает 1 аргумент Real (обозначенный Tuple{Real}
) и возвращает результат Real
. Такой вызываемый Sym
можно вызвать с помощью числового или символьного выражения допустимого типа.
Это выражение также определяет t
как независимую переменную, тогда как x(t)
и y(t)
являются зависимыми переменными. Это учитывается при дифференцировании:
z = x + y*t
expand_derivatives(D(z))
y(t) + Differential(t)(x(t)) + t*Differential(t)(y(t))
Поскольку x
и y
зависят от времени, они не исключаются из выражения автоматически, и поэтому операции D(x)
и D(y)
по-прежнему присутствуют в разложенных производных в целях корректности.
Мы также можем определить неограниченные функции:
@variables g(..)
1-element Vector{Symbolics.CallWithMetadata{SymbolicUtils.FnType{Tuple, Real}, Base.ImmutableDict{DataType, Any}}}:
g⋆
Здесь g
— это переменная, являющаяся функцией других переменных. Каждый раз, когда мы указываем g
, мы должны использовать ее в качестве функции.
z = g(x) + g(y)
g(y(t)) + g(x(t))
Регистрация функций
Одним из преимуществ одноязыкового символьного стека Julia является то, что все примитивы написаны на Julia и поэтому он тривиально расширяем из самого языка Julia. По умолчанию новые функции прослеживаются до примитивов, а символьные выражения записываются в примитивах. Однако можно расширить разрешенные примитивы, зарегистрировав новые функции. Например, зарегистрируем новую функцию h
:
h(x, y) = x^2 + y
@register_symbolic h(x, y)
Когда мы используем h(x, y)
, оно представляет собой символьное выражение и не расширяется:
h(x, y) + y^2
Main.h(x(t), y(t)) + y(t)^2
Чтобы использовать его в системе дифференцирования, нужно зарегистрировать его производные. Это можно сделать так.
# Производная по первому аргументу.
Symbolics.derivative(::typeof(h), args::NTuple{2,Any}, ::Val{1}) = 2args[1]
# Производная по второму аргументу.
Symbolics.derivative(::typeof(h), args::NTuple{2,Any}, ::Val{2}) = 1
и теперь оно работает с оставшейся частью системы:
Symbolics.derivative(h(x, y) + y^2, x)
2x(t)
Symbolics.derivative(h(x, y) + y^2, y)
1 + 2y(t)
Создание функций
Функцией для построения функций из символьных выражений является метко названная build_function
. Первый аргумент — это символьное выражение или массив символьных выражений для компиляции, а последующие аргументы — это аргументы для функции. Например:
to_compute = [x^2 + y, y^2 + x]
f_expr = build_function(to_compute, [x, y])
Base.remove_linenums!.(f_expr)
(:(function (ˍ₋arg1,)
begin
begin
(SymbolicUtils.Code.create_array)(typeof(ˍ₋arg1), nothing, Val{1}(), Val{(2,)}(), (+)(ˍ₋arg1[2], (^)(ˍ₋arg1[1], 2)), (+)(ˍ₋arg1[1], (^)(ˍ₋arg1[2], 2)))
end
end
end), :(function (ˍ₋out, ˍ₋arg1)
begin
begin
#= /root/.julia/packages/SymbolicUtils/ssQsQ/src/code.jl:422 =# @inbounds begin
ˍ₋out[1] = (+)(ˍ₋arg1[2], (^)(ˍ₋arg1[1], 2))
ˍ₋out[2] = (+)(ˍ₋arg1[1], (^)(ˍ₋arg1[2], 2))
nothing
end
end
end
end))
возвращает два кода. Первый представляет собой функцию f([x, y])
, которая вычисляет и создает выходной вектор [x^2 + y, y^2 + x]
. Поскольку этот инструмент был создан для всех специалистов, пишущих быстрый код на Julia, он специализирован для Julia и поддерживает такие функции, как StaticArrays. Например:
using StaticArrays
myf = eval(f_expr[1])
myf(SA[2.0, 3.0])
2-element StaticArraysCore.SVector{2, Float64} with indices SOneTo(2):
7.0
11.0
Вторая функция — это не поддерживающая выделение изменяемая функция на месте, которая изменяет свое первое значение. Поэтому мы используем ее следующим образом.
myf! = eval(f_expr[2])
out = zeros(2)
myf!(out, [2.0, 3.0])
out
2-element Vector{Float64}:
7.0
11.0
Чтобы сохранить символьные вычисления для использования в дальнейшем, мы можем взять это выражение и сохранить его в файл:
write("function.jl", string(f_expr[2]))
366
Обратите внимание, что если нужно избежать eval
, например чтобы не допустить проблем с возрастом мира, можно сделать expression = Val{false}
:
Base.remove_linenums!(build_function(to_compute, [x, y], expression=Val{false})[1])
RuntimeGeneratedFunction(#=in Symbolics=#, #=using Symbolics=#, :((ˍ₋arg1,)->begin
#= /root/.julia/packages/SymbolicUtils/ssQsQ/src/code.jl:373 =#
#= /root/.julia/packages/SymbolicUtils/ssQsQ/src/code.jl:374 =#
#= /root/.julia/packages/SymbolicUtils/ssQsQ/src/code.jl:375 =#
begin
begin
#= /root/.julia/packages/SymbolicUtils/ssQsQ/src/code.jl:468 =#
(SymbolicUtils.Code.create_array)(typeof(ˍ₋arg1), nothing, Val{1}(), Val{(2,)}(), (+)(ˍ₋arg1[2], (^)(ˍ₋arg1[1], 2)), (+)(ˍ₋arg1[1], (^)(ˍ₋arg1[2], 2)))
end
end
end))
где будет использоваться RuntimeGeneratedFunctions.jl для создания функций Julia, исключающих возникновение проблем с возрастом мира.