Динамический блок в Engee
Пользовательский блок с переменным количеством портов
В своих постах раннее я рассказывал как сделать свой блок при помощи Engee Function. Вот только речь шла о блоках, в которых количество входов и выходов уже известно. В этой публикации я покажу как сделать блок с переменным количеством входов на примере сумматора.
Реализация сумматора
Структура блока
Наш сумматор должен уметь складывать свои входы. Как и в классическом сумматоре, каждому входу соответсвует знак + или -.
Если пользователь ввел "+-", то сумматор вычислит выход как:
Иными словами, формула сумматора следующая:
Следовательно, требуется где-то хранить список знаков для входов.
Поэтому у блока Engee Function будет параметр: вектор знаков, а структура блока будет описана как:
mutable struct Block <: AbstractCausalComponent
port_values::Vector{Float64}
function Block()
vects_length = length(INPUT_SIGNAL_ATTRIBUTES)
port_values = zeros(vects_length,)
new(port_values)
end
end
Обратите внимание, что так как количество входов в сумматоре переменное, то длина вектора знаков будет рассчитываться при инициализации блока на основе размера внутренней переменной INPUT_SIGNAL_ATTRIBUTES.
Обработка неизвестного количества портов
Главная проблема с которой я столкнулся при разработке блока с переменным количеством портов - как их обработать. Функтор блока имеет сигнатуру:
function (c::Block)(t::Real, in1, in2)
Нам надо получить список входов. Для этого применим макрос Base.@locals, который вернет словарь с именами и значениями локальных переменных, в том числе и входов.
Важно
Макрос возвращает все локальные переменные, поэтому его надо вызывать сразу после захода в функтор
Вот как выглядит код для получения всех входов:
args_dict = Base.@locals
pat = r"^in(\d)";
i = 1;
for (name, value) in args_dict
if occursin(pat, String(name))
c.port_values[i] = value
i = i+1
end
end
Обратите внимание, что я ищу входы по совпадению строки "in<N>" с помощью регулярных выражений.
Совет
Очень сложно написать нужное регулярное выражение вручную. Используйте любой онлайн конструктор!
Общий вид кода функтора блока такой:
function (c::Block)(t::Real, in1, in2)
args_dict = Base.@locals
pat = r"^in(\d)";
i = 1;
for (name, value) in args_dict
if occursin(pat, String(name))
c.port_values[i] = value
i = i+1
end
end
r = (c.port_values .* sign_in)
return sum(r)
end
Делаем маску
Скроем нашу реализацию от пользователя - добавим маску. Наша маска должна не только дать пользователю ввести список знаков, но и:
- Валидировать вход
- Настроить блок Engee Function
- Выставить параметры
То есть, маска должна быть "умной" и без написания кода тут не обойтись.
Валидация ввода
Для валидации ввода можно использовать два подхода:
-
Использовать обратный вызов validateCallback у параметра:
-
Использовать обратный вызов blockChangedCallback самой маски:
Для нашей задачи буду применять последний способ.
Что мы будем проверять:
- Пользователь ввел строку
- Минимальная длина строки - 2
- Строка содержит только "+" и "-"
Проверку будем делать через макрос @assert:
@assert typeof(inp_signs) == String
@assert length(inp_signs) >= 2
@assert occursin(r"^[+-]+$",inp_signs)
И опять мы столкнулись с регулярными выражениями!
Настройка блока
Вспомним как работает сумматор: пользователь вводит строку из + и - и блок меняет количество входов. Поэтому код маски должен уметь следующее:
- Настраивать количество входов Engee Function
- Разбирать пользовательский ввод
Делать это будем вот так:
blk = engee.gcb()
vec = Vector{Int64}();
engee.set_param!(blk,"Inputs"=>length(inp_signs))
for (ind,char) in enumerate(inp_signs)
append!(vec,char == '+' ? 1 : -1)
end
engee.set_param!(blk,"Parameter1Value"=>vec)
for i in 1:length(inp_signs)
engee.set_param!(blk,"InputPort$(i)Size"=>())
end
Главная идея - использовать командное управление для настройки Engee Function.
Значение параметра нашей Engee Function динамически устанавливается с помощью следующего кода:
for (ind,char) in enumerate(inp_signs)
append!(vec,char == '+' ? 1 : -1)
end
engee.set_param!(blk,"Parameter1Value"=>vec)
Давайте проверим, что сумматор работает. Вычислим косинус из синуса, согласно известной формуле:
Для этого соберем такую модель:
и запустим ее симуляцию:
mdl = engee.load(joinpath(@__DIR__,"ef_variadic.engee"))
res = engee.run(mdl)
sine = collect(res["sin_mod"])
cosine = collect(res["cos"])
plot(sine.time,sine.value,label="sine")
plot!(cosine.time,cosine.value,label="cosine")