Создание блоков Engee Function с переменными состояния
В этом примере мы создадим блок Engee Function, параметры которого могут изменяться во времени в зависимости от различных факторов. Обычно вычисление выходных данных пользовательского блока происходит на основании входных данных и вектора времени. Но в этом примере мы покажем, как работать с переменными, которые могут хранить в себе внутреннее состояние блока.
В практическом плане – мы покажем, как изменять результат вычисления пользовательского блока в зависимости от внутреннего счетчика выполнений, реализованного внутри блока.
Организация параметров блока Engee Function
Параметры блока Engee Function можно задать разными способами:
- при помощи констант, через окно настроек
Parameters - при помощи переменных из глобального пространства переменных Engee (видны в Окне Переменных)
- при помощи внутренних параметров блока, заданных в его коде (причем время их существования не ограничивается очередным циклом вычислений, а равно времени существования модели).
Первые два варианта реализуются почти одинаково. Если количество параметров – ненулевое, то для каждого из них нужно задать имя Name и значение Value.
Здесь мы задали размер окна для вычисления статистических параметров max_len_init и присвоили ему значение 100.
Мы могли бы присвоить ему значение N, заданное в глобальном пространстве переменных. Следует иметь в виду ограничение компилятора моделей: при передаче переменной из глобального пространства, имя параметра (Name) не должно быть идентично имени глобальной переменной (Value).

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

Здесь MA означает Moving Average (скользящее среднее), а MD – Moving 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).
Запуск модели и обсуждение результатов
Запустим эту модель при помощи команд программного управления:
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 )
Выведем результат:
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)
)
На левом графике изображен шумовой сигнал, который поступал на вход в пользовательский блок Engee Function. На правом графике изображена зависимость скользящего среднего значения от значения дисперсии для скользящего окна.
Можно отметить, что отправной точкой среднего значения и дисперсии было значение для нулевой выборки, но через несколько десятков шагов оценка этих параметров сместилась к точке , . К концу расчетного периода, когда инициализация скользящего окна перестала сказываться на статистике, точка на графике стала заметно смещаться к нулевому математическому ожиданию при .
Заключение
Если добавить к исходному шаблону кода в компоненте Engee Function несколько стандартных конструкций, то в этом компоненте можно воплотить очень сложное поведение, опирающееся на внешний код и изменяемые параметры. В Engee существует много графических и текстовых способов описания поведения моделей, в дополнение к которым можно пользоваться динамическими блоками с пользовательским кодом внутри.