Engee 文档
Notebook

基于Engee函数的高级块开发

工程项目说明

我们已经学会了如何[使用Engee函数开发我们的块](https://engee.com/community/ru/catalogs/projects/prodvinutoe-ispolzovanie-engee-function )。

但是,如果要实现的算法是复杂和繁琐的呢? 问题立即出现:

*Engee函数代码原来是臃肿的,只是很难阅读。

*目前还不清楚如何测试这样的代码。 如果发生错误,则很难对其进行本地化。

解决这些问题不是"火箭科学",而是程序员的日常工作。 所以让我们成为他们一段时间!

这就是我们将如何解决繁琐代码的问题。:

  1. 让我们将算法分离成一个单独的模块

  2. 我们将用测试复盖模块

  3. 让我们增加代码的稳定性

作为一个例子,考虑一种算法,用于查找两组观测值之间的距离。 为了工作,我们需要以下软件包:

In [ ]:
import Pkg
Pkg.add(["LinearAlgebra", "Test", "BenchmarkTools"])
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`

代码删除到模块

Julia上下文中的模块是包含在单独命名空间中的代码。 这允许您使该模块内部的变量在其外部"不可见"。 如欲了解更多有关这些模块的优点,请参阅帮助

让我们看看模块中包含的算法代码:

In [ ]:
;cat PDIST2.jl
module PDIST2

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

测试

测试的目的是证明代码工作正常,并且捕获所有异常。

测试是用[测试]编写的。jl]包(https://engee.com/helpcenter/stable/ru/julia/stdlib/Test.html

让我们创建三个简单的测试:

*简单的函数调用

*不同尺寸的加工

*计算相同矩阵之间距离的正确性

让我们把这些测试放在一个测试套件中,它被定义为:

``'茱莉亚
@testset开始
<测试>
结束


测试包中测试的一个特殊功能。Jl是测试宏验证某个表达式的真实性。 我们来看一个例子:

In [ ]:
using Test, LinearAlgebra

demoroot = @__DIR__

include("PDIST2.jl")

X = rand(3,3)
Y = rand(3,3)
Z = PDIST2.EF_pdist2(X,Y);
@testset "EF_pdist2 tests" begin
    @test_nowarn PDIST2.EF_pdist2(X,Y);
    @test_throws DimensionMismatch PDIST2.EF_pdist2(rand(3,3),rand(2,2))
    @test iszero(diag(PDIST2.EF_pdist2(X,X)))
end
Test Summary:   | Pass  Total  Time
EF_pdist2 tests |    3      3  0.8s
Out[0]:
Test.DefaultTestSet("EF_pdist2 tests", Any[], 3, false, false, true, 1.754979865285884e9, 1.754979866072697e9, false, "In[3]")

工作表现评估

要评估代码的整体性能,您需要测量几个指标:

*执行速度

*分配的内存量

*分配数量(特定于Julia)

为此,我们将使用基准。jl包。 它的优点在于您可以微调实验或立即开始测量。:

In [ ]:
using BenchmarkTools

@benchmark PDIST2.EF_pdist2(rand(10,10),rand(10,10))
Out[0]:
BenchmarkTools.Trial: 10000 samples with 8 evaluations per sample.
 Range (minmax):  3.033 μs 9.495 ms   GC (min … max):  0.00% … 99.82%
 Time  (median):     7.843 μs               GC (median):     0.00%
 Time  (mean ± σ):   9.099 μs ± 95.130 μs   GC (mean ± σ):  10.42% ±  1.00%

                      ▁▄▇██▄▂▁                               
  ▂▃▃▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▂▅████████▇▆▄▃▃▂▂▂▂▂▂▂▂▂▁▁▂▁▁▁▁▁▁▁▁▁▁▁ ▂
  3.03 μs        Histogram: frequency by time        14.2 μs <

 Memory estimate: 6.77 KiB, allocs estimate: 18.

以下测量对我们很重要:时间和内存估计。

时间是单次运行的执行时间。 由于@benchmark运行了几次运行,我们得到了一组这样的测量值,并且可以对其应用统计处理。 运行@benchmark宏后显示其结果,如上所示。

内存估计将显示分配的内存量。 让我们看看执行时间和内存量是如何随着输入量的增加而增加的。:

In [ ]:
matrix_size = 80 # @param {type:"slider",min:10,max:100,step:10}
@benchmark PDIST2.EF_pdist2(rand(matrix_size,matrix_size),rand(matrix_size,matrix_size))
Out[0]:
BenchmarkTools.Trial: 10000 samples with 1 evaluation per sample.
 Range (minmax):   96.698 μs 23.746 ms   GC (min … max):  0.00% … 98.90%
 Time  (median):     116.496 μs                GC (median):     0.00%
 Time  (mean ± σ):   172.030 μs ± 430.871 μs   GC (mean ± σ):  13.25% ±  6.23%

  ▁▇█▆▆▄▂▁                                            ▁▃▃▂▂▁   ▂
  ██████████▇▇▆▆▄▃▁▃▁▁▁▃▁▁▁▁▁▁▁▁▁▁▁▃▆▇█▇▄▄▁▁▁▃▅▇▇▆▆▄▇███████▇ █
  96.7 μs       Histogram: log(frequency) by time        494 μs <

 Memory estimate: 352.01 KiB, allocs estimate: 25.

容错能力

您创建的代码必须具有抗错误性。 这需要处理异常。

一般意义上的异常是可以处理的任何错误。 与通常的错误(语法错误)的主要区别在于用户可以自己生成异常。 让我们来看看我们的pdist2函数的代码:

``'茱莉亚
m,n=尺寸(X)
p,n2=尺寸(Y)
n==n2||throw(DimensionMismatch("X和Y中的列数必须匹配"))


如果矩阵的维度不匹配,则调用throw函数并抛出异常。

异常可以使用构造来处理

``'茱莉亚
试试
渔获
结束

代码调用被放置在try块中。 如果此代码引发异常,则将执行catch块中的代码。

在Engee函数中使用库

例如,我们将使用EF_dist_find模型。:

image.png
In [ ]:
mdl = engee.open(joinpath(demoroot,"EF_dist_find.engee"));

考虑PDIST2块构造函数:

``'茱莉亚
include("/user/start/examples/base_simulation/advanced_block_development/PDIST2.jl")

可变结构块<:AbstractCausalComponent
缓存:矩阵{Float64};
功能块()
c=零(Float64,INPUT_SIGNAL_ATTRIBUTES[1]。尺寸);
info("分配$(Base.summarysize(c))字节为pdist2")
新(c)
结束

结束


我们将我们的模块包含在Engee函数中,指定其代码的完整路径。

使用##info()##函数,我们将显示创建缓存时分配的内存量。

考虑此块的步骤方法:

``'茱莉亚
函数(c::块)(t::实,in1,in2)    
    试试
        c.缓存=PDIST2。EF_pdist2(in1,in2);
    渔获
        错误("矩阵维度应该相等!")
        停止模拟()
    结束

    返回c.缓存
结束

请注意,来自我们模块的函数调用被包装在异常处理程序中。 如果我们的pdist2函数抛出异常,则错误"矩阵维数应该相等!"将出现在诊断窗口中,模拟将停止。

打开时,创建两个带有随机数的3x3矩阵。 让我们确保模型工作。:

In [ ]:
engee.run(mdl)
Out[0]:
SimulationResult(
    "PDIST2.1" => WorkspaceArray{Matrix{Float64}}("EF_dist_find/PDIST2.1")

)

结论

该项目展示了一种创建基于Engee函数的块的方法,这使得调试更容易,并提高了代码的质量和可靠性。

示例中使用的块