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

Пользовательский блок с переменным количеством портов

В своих постах раннее я рассказывал как сделать свой блок при помощи 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

Делаем маску

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

  1. Валидировать вход
  2. Настроить блок Engee Function
  3. Выставить параметры

То есть, маска должна быть "умной" и без написания кода тут не обойтись.

Валидация ввода

Для валидации ввода можно использовать два подхода:

  1. Использовать обратный вызов validateCallback у параметра:

    image.png
  2. Использовать обратный вызов blockChangedCallback самой маски:

    image.png

Для нашей задачи буду применять последний способ.

Что мы будем проверять:

  1. Пользователь ввел строку
  2. Минимальная длина строки - 2
  3. Строка содержит только "+" и "-"

Проверку будем делать через макрос @assert:

@assert typeof(inp_signs) == String
@assert length(inp_signs) >= 2
@assert occursin(r"^[+-]+$",inp_signs)

И опять мы столкнулись с регулярными выражениями!

Настройка блока

Вспомним как работает сумматор: пользователь вводит строку из + и - и блок меняет количество входов. Поэтому код маски должен уметь следующее:

  1. Настраивать количество входов Engee Function
  2. Разбирать пользовательский ввод

Делать это будем вот так:

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)

Давайте проверим, что сумматор работает. Вычислим косинус из синуса, согласно известной формуле:

Для этого соберем такую модель:

image.png

и запустим ее симуляцию:

In [ ]:
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")
Out[0]: