Сообщество Engee

Еще один эксперимент с масками Engee Function

Автор
avatar-nikfilaretovnikfilaretov
Notebook

Маски и строки в Engee Function

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

У функции EF_pdist2, которую мы использовали, есть необязательный параметр - режим расчета (метрика). В ходе проекта дадим пользователю возможность управлять им через маску.

In [ ]:
include("PDIST2.jl");

Давайте вспомним как выглядит код модуля:

In [ ]:
;cat PDIST2.jl
module PDIST2

mode_dict_t = Dict(1=>"euclidean",2=>"squaredeuclidean",3=>"manhattan",4=>"cosine")

function EF_pdist2(X::Matrix{Float64}, Y::Matrix{Float64}; metric::String="euclidean")
    m, n = size(X)
    p, n2 = size(Y)
    n == n2 || throw(DimensionMismatch("Number of columns in X and Y must match"))

    if metric == "euclidean"
        XX = sum(X.^2, dims=2)
        YY = sum(Y.^2, dims=2)
        D = XX .+ YY' .- 2 .* (X * Y')
        return sqrt.(max.(D, 0))
    
    elseif metric == "squaredeuclidean"
        XX = sum(X.^2, dims=2)
        YY = sum(Y.^2, dims=2)
        D = XX .+ YY' .- 2 .* (X * Y')
        return max.(D, 0)
    
    elseif metric == "manhattan"
        D = zeros(m, p)
        for j in 1:p
            for i in 1:m
                D[i, j] = sum(abs.(X[i, :] .- Y[j, :]))
            end
        end
        return D
    
    elseif metric == "cosine"
        XX = sqrt.(sum(X.^2, dims=2))
        YY = sqrt.(sum(Y.^2, dims=2))
        norms = XX .* YY'
        XY = X * Y'
        
        # Handle division by zero: set invalid entries to 0, then correct cases where both vectors are zero
        sim = zeros(size(XY))
        valid = norms .> 0
        sim[valid] .= XY[valid] ./ norms[valid]
        
        # Identify pairs where both vectors are zero (cosine similarity = 1)
        both_zero = (XX .== 0) .& (YY' .== 0)
        sim[both_zero] .= 1
        
        return 1 .- sim
    
    else
        throw(ArgumentError("Unknown metric: $metric. Supported metrics are 'euclidean', 'squaredeuclidean', 'manhattan', 'cosine'"))
    end
end


end

Как передать строку из маски в Engee Function?

Перед тем как маскировать Engee Function, требуется рассмотреть функциональное ограничение - Engee Function не умеет работать с параметрами типа String. Но метод расчета задается строкой. Следовательно, нам надо закодировать выбранный метод некоторым числом, передать это число как параметр в Engee Function, а потом декодировать его. То есть в маске понадобится не один параметр, а два. Тогда можно реализовать такую схему:

image.png

Декодировать режим расчета будем при помощи словаря, так как тип данных Dict (Словарь) реализует коллекцию пар "ключ-значение". При этом ключом может быть любой базовый тип. Следовательно, возможно сделать даже такой словарь:

mode_dict_t = Dict(1=>"euclidean",2=>"squaredeuclidean",3=>"manhattan",4=>"cosine")

Видно, что ключом здесь выступает целое, а значением - строка. Это позволяет нам сделать следующее:

In [ ]:
EF_mode = PDIST2.mode_dict_t[1]
Out[0]:
"euclidean"

Теперь взглянем на код методов блока Engee Function:

include("/user/start/examples/base_simulation/advanced_block_masking/PDIST2.jl")

mutable struct Block <: AbstractCausalComponent
	cache::Matrix{Float64};
	modeC::Int64
	function Block()
    		c = zeros(Float64,INPUT_SIGNAL_ATTRIBUTES[1].dimensions);
    		info("Allocated $(Base.summarysize(c)) bytes for pdist2")
    		info("Selected $(PDIST2.mode_dict_t[mode])")
    		new(c,mode)
	end
end

function (c::Block)(t::Real, in1, in2)
    try
        c.cache = PDIST2.EF_pdist2(in1,in2;metric=PDIST2.mode_dict_t[c.modeC]);
    catch
        error("Matrix Dimensions should be equal!")
        stop_simulation()
    end

    return c.cache
end

modeC - это параметр, который принимает значение числовой переменной из маски (см. ). Тем самым, мы можем передать в нашу функцию PDIST2 метод расчета как

PDIST2.mode_dict_t[c.modeC]

А число мы получим с помощью обратного вызова "реального" параметра из выпадающего списка:

if mask.parameters.mask_mode.value == "euclidean"
    mask.parameters.mask_mode_int.value = 1;
elseif mask.parameters.mask_mode.value == "squaredeuclidean"
    mask.parameters.mask_mode_int.value = 2;
elseif mask.parameters.mask_mode.value == "manhattan"
    mask.parameters.mask_mode_int.value = 3;
elseif mask.parameters.mask_mode.value == "cosine"
    mask.parameters.mask_mode_int.value = 4;
end

Таким образом маска будет выглядеть следующим образом:

image.png

Обратите внимание, что параметр mask_mode_int должен быть вычисляемым. А скрыт он, так как мы не хотим давать пользователю что-нибудь сломать.

image.png

Наконец, откроем и запустим итоговую модель во всех режимах:

In [ ]:
demoroot = @__DIR__
mdl = engee.open(joinpath(demoroot,"EF_DF.engee"))
Out[0]:
System(
    name: root,
    id: be446814-cf00-421d-ad5b-484c5f63ca08,
    path: EF_DF
)
In [ ]:
for k in keys(PDIST2.mode_dict_t)
    println("Запускаем модель с режимом $(PDIST2.mode_dict_t[k])")
    engee.set_param!("EF_DF/PDIST2", "mask_mode"=>PDIST2.mode_dict_t[k])
    engee.run(mdl;verbose=true);
end
Запускаем модель с режимом cosine
Building...
Progress 0%
Progress 100%
Progress 100%
Запускаем модель с режимом squaredeuclidean
Building...
Progress 0%
Progress 100%
Progress 100%
Запускаем модель с режимом manhattan
Building...
Progress 0%
Progress 100%
Progress 100%
Запускаем модель с режимом euclidean
Building...
Progress 0%
Progress 100%
Progress 100%

Выводы

В ходе проекта мы узнали, как пробрасывать строковые параметры в Engee Function, а так же использовали словари штатным, хоть и неочевидным способом.