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

Создание блоков Engee Function с переменными состояния

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

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

Организация параметров блока Engee Function

Параметры блока Engee Function можно задать разными способами:

  • при помощи констант, через окно настроек Parameters
  • при помощи переменных из глобального пространства переменных Engee (видны в Окне Переменных)
  • при помощи внутренних параметров блока, заданных в его коде (причем время их существования не ограничивается очередным циклом вычислений, а равно времени существования модели).

Первые два варианта реализуются почти одинаково. Если количество параметров – ненулевое, то для каждого из них нужно задать имя Name и значение Value.

image_3.png

Здесь мы задали размер окна для вычисления статистических параметров max_len_init и присвоили ему значение 100.

Мы могли бы присвоить ему значение N, заданное в глобальном пространстве переменных. Следует иметь в виду ограничение компилятора моделей: при передаче переменной из глобального пространства, имя параметра (Name) не должно быть идентично имени глобальной переменной (Value).

image_2.png

Это временное поведение компилятора скоро будет исправлено.

Переменные параметры в коде Engee Function

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

Блок Engee Function будет принимать на вход скалярные значения и накапливать по ним статистику. На входе будет находиться блок генерации шума – равномерно распределенной случайной величины со значениями в интервале (-1,1). Интерфейс блока будет выглядеть следующим образом:

image_2.png

Здесь MA означает Moving Average (скользящее среднее), а MDMoving Deviation (скользящая дисперсия).

Стоит прокомментировать каждую секцию кода, которая задает поведение нашего пользовательского блока.

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

  • i – счетчик последнего добавленного элемента в циклическом накопителе
  • max_len – размер скользящего окна
  • X – накопитель выборки (вектор размером max_len)

В функции Block() внутренние параметры получают конкретные значения, в том числе получаемые извне (размер окна max_len_init задается на вкладке Parameters).

Разбор кода внутри компонента Engee Function

Стоит внимательно подойти к вопросу назначения типов данных параметрам блока. Если не объявить структуру данных, задающую переменные состояния, как структуру типа mutable, то все "простые" типы в составе структуры будут неизменными (придется пользоваться векторами или ссылками, например i :: Ref{Int32};). Мы же хотим, чтобы параметры можно было изменить. Поэтому мы задаем специальный модификатор нашей структуре:

mutable struct Block <: AbstractCausalComponent

    i :: Int32;
    max_len :: Int32;
    X :: Vector{Float64};

    function Block()
        return new( 1, max_len_init, zeros(Float64, max_len_init) )
    end

end

Затем в коде мы видим функцию, которая вычисляется при каждом обращении к блоку.

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

Здесь мы можем обращаться к внешним и внутренним параметрам блока (c.параметр).

function (c::Block)(t::Real, x)

    # Локальные переменные нужны только для удобства чтения
    X = c.X
    N = c.max_len

    MA = sum( X[1:N] )/N;
    MD = sqrt( sum( (X[1:N] .- MA).^2 ) / (N-1) )

    return (MA, MD)
end

И наконец мы видим функцию update!, которая призвана вносить изменения в параметры блока c.

  • Мы подставляем текущее значение x со входом блока Engee Function в вектор X по индексу i;
  • Затем мы увеличиваем на 1 индекс i, а при достижении им значения max_len, уменьшаем обратно до единицы;
  • И наконец возвращаем обновленную структуру параметров c для обращения к ней же уже на следующей итерации.
function update!(c::Block, t::Real, x)
    
    c.X[c.i] = x
    c.i = max(1, (c.i + 1) % (c.max_len-1))
    
    return c
end

Код внутри update! особенно важен в тех случаях, когда блок Engee Function находится в алгебраической петле (например, принимает на вход собственные выходы или имеет выставленный параметр direct_feedthrough=false). В моделях, где нет петли, всё можно сделать во втором блоке кода (функторе step).

Запуск модели и обсуждение результатов

Запустим эту модель при помощи команд программного управления:

In [ ]:
mName = "engee_function_moving_average"
model = mName in [m.name for m in engee.get_all_models()] ? engee.open( mName ) : engee.load( "$(@__DIR__)/$(mName).engee" );
data = engee.run( mName )
Out[0]:
Dict{String, DataFrame} with 3 entries:
  "MA"  => 501×2 DataFrame…
  "src" => 501×2 DataFrame…
  "MD"  => 501×2 DataFrame

Выведем результат:

In [ ]:
using Plots, Formatting

p = plot( data["MD"].value, data["MA"].value, label="MA(MD)", linez=range(0.4, 1, length(data["MA"].value)), c=:blues, cbar=:none)
# Последняя точка на графике
scatter!( p, [data["MD"].value[end]], [data["MA"].value[end]], c=:cyan )
# Текстовая подпись к этой точке 
annotate!( p, data["MD"].value[end], data["MA"].value[end],
           text("MA=$(sprintf1("%.2f",data["MD"].value[end])), MD=$(sprintf1("%.2f",data["MA"].value[end]))  ", 8, :right ),
           legend=:none )

# Вывод двух графиков
plot(
    # График слева: шум
    plot( data["src"].time, data["src"].value, label="input", c=:red, legend=:none ),
    # График справа – зависимость мат.ожидания от дисперсии
    p,
    size=(800,300)
)
Out[0]:

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

Можно отметить, что отправной точкой среднего значения и дисперсии было значение для нулевой выборки, но через несколько десятков шагов оценка этих параметров сместилась к точке $\mu \approx -0.15$, $\sigma \approx 0.55$. К концу расчетного периода, когда инициализация скользящего окна перестала сказываться на статистике, точка на графике стала заметно смещаться к нулевому математическому ожиданию при $\sigma \approx 0.6$.

Заключение

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

Блоки, использованные в примере