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

Начало работы с Symbolics.jl

Symbolics.jl — это язык символьного моделирования. В этом руководстве вы узнаете, как запустить 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, исключающих возникновение проблем с возрастом мира.