Создание блоков Engee Function с переменными состояния
В этом примере мы создадим блок Engee Function
, параметры которого могут изменяться во времени в зависимости от различных факторов. Обычно вычисление выходных данных пользовательского блока происходит на основании входных данных и вектора времени. Но в этом примере мы покажем, как работать с переменными, которые могут хранить в себе внутреннее состояние блока.
В практическом плане – мы покажем, как изменять результат вычисления пользовательского блока в зависимости от внутреннего счетчика выполнений, реализованного внутри блока.
Организация параметров блока Engee Function
Параметры блока Engee Function
можно задать разными способами:
-
при помощи констант, через окно настроек
Parameters
-
при помощи переменных из глобального пространства переменных Engee (видны в Окне Переменных)
-
при помощи внутренних параметров блока, заданных в его коде (причем время их существования не ограничивается очередным циклом вычислений, а равно времени существования модели).
Первые два варианта реализуются почти одинаково. Если количество параметров – ненулевое, то для каждого из них нужно задать имя Name
и значение Value
.
Здесь мы задали размер окна для вычисления статистических параметров nMA
и присвоили ему значение 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()
внутренние параметры получают конкретные значения, в том числе получаемые извне (размер окна nMA
задается на вкладке Parameters
).
Разбор кода внутри компонента Engee Function
Стоит внимательно подойти к вопросу назначения типов данных параметрам блока. Если не объявить структуру данных, задающую переменные состояния, как структуру типа mutable
, то все "простые" типы в составе структуры будут неизменными (придется пользоваться векторами или ссылками, например i :: Ref{Int32};
). Мы же хотим, чтобы параметры можно было изменить. Поэтому мы задаем специальный модификатор нашей структуре:
mutable struct Block <: AbstractCausalComponent
i :: Int32;
max_len :: Int32;
X :: Vector{Float64};
function Block()
return new( 1, nMA, zeros(Float64, nMA) )
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 )
Dict{String, DataFrames.DataFrame} with 3 entries:
"MA" => 501×2 DataFrame…
"src" => 501×2 DataFrame…
"MD" => 501×2 DataFrame…
Выведем результат:
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 существует много графических и текстовых способов описания поведения моделей, в дополнение к которым можно пользоваться динамическими блоками с пользовательским кодом внутри.