Engee 文档
Notebook

使用状态变量创建 Engee 功能块

在本示例中,我们将创建一个块Engee Function ,其参数可随时间变化,这取决于各种因素。通常,自定义程序块的输出是根据输入数据和时间矢量计算得出的。但在本示例中,我们将展示如何使用变量来存储程序块的内部状态。

在实际应用中,我们将展示如何根据程序块内部的执行计数器来改变自定义程序块的计算结果。

Engee 功能块参数的组织

功能块Engee Function 的参数可通过不同方式设置:

  • 通过设置窗口使用常量Parameters
  • 使用全局变量空间Engee中的变量(在变量窗口中可见)
  • 借助代码中指定的程序块内部参数(参数存在时间不受下一轮计算限制,与模型存在时间相等)。

前两种变量的实现方式几乎相同。如果参数个数不为零,则需要为每个参数设置名称Name 和值Value

image_3.png

在这里,我们设置了用于计算统计参数的窗口大小max_len_init ,并为其赋值100



.

我们本可以将全局变量空间中设置的值N 分配给它。我们应该记住模型编译器的一个限制:从全局变量空间传递变量时,参数名称 (Name) 不能与全局变量名称 (Value) 相同。

image_2.png

这种临时编译器行为将很快得到修复。

Engee 函数代码中的变量参数

让我们来看一个实际例子。我们要实现一个程序块,计算属于给定大小滑动窗口的样本部分的统计参数。滑动窗口的初始值为零,因此前 N 个结果将是有偏差的,但随后我们将看到滑动窗口的统计数据,包括观察样本中最后N 个值。

Engee Function 模块将标量值作为输入,并对其进行统计累计。噪声生成块是一个均匀分布的随机变量,其取值范围为 (-1,1)。程序块的界面如下:

image_2.png

MA 代表Moving Average (移动平均值),MD 代表Moving Deviation (移动方差)。

值得注意的是,要对定义自定义程序块行为的每段代码进行注释。

代码从初始化用户块结构开始。Block 这个名字是随机选择的,可以是任何名字。我们在其中设置了几个参数:

*i - 循环累加器中最后添加元素的计数器 *max_len - 滑动窗口的大小 *X - 取样累加器(max_len 大小的向量)

Block() 函数中,内部参数接收特定的值,包括从外部接收的值(max_len_init 窗口的大小在Parameters 标签页上设置)。

解析 Engee 函数组件内部的代码

在为块参数指定数据类型时,我们应该小心谨慎。如果不将定义状态变量的数据结构声明为mutable 类型的结构,那么结构中的所有 "简单 "类型都将保持不变(必须使用向量或引用,例如i :: Ref{Int32}; )。我们希望能够修改参数。因此,我们为结构设置了一个特殊的修改器:

``朱莉娅 可变结构体 Block <: AbstractCausalComponent

i :: Int32;
max_len :: Int32;
X :: 矢量{Float64};

函数 Block()
    return new( 1, max_len_init, zeros(Float64, max_len_init) ) )
结束

结束


然后,在代码中我们会看到一个函数,每次访问该代码块时都会对该函数进行评估。

非常重要的是,在调用该函数时,其内部变量不会发生变化。在模拟步骤之间,方法(函数)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) )

返回 (MA, MD)

结束


最后,我们看到函数update! ,它的目的是修改程序块c 的参数。

  • 我们将x 的当前值与程序块输入值Engee Function 一起代入矢量X 的索引i
  • 然后,我们将i 的索引值增加 1,当它达到max_len 的值时,我们将它减回 1;
  • 最后,我们返回更新后的参数结构c ,以便下次迭代时引用。

``朱莉娅 function update!(c::Block, t::Real, x)

c.X[c.i] = x
c.i = max(1, (c.i + 1) % (c.max_len-1))

返回 c

结束


Engee Function 代码块处于代数循环中时(例如,它把自己的输出作为输入,或有一个暴露的参数direct_feedthrough=false ),update! 内的代码就显得尤为重要。在没有循环的模型中,一切都可以在第二个代码块(函数step )中完成。

运行模型并讨论结果

让我们使用程序控制命令运行该模型:

In [ ]:
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 )
Out[0]:
Dict{String, DataFrame} with 3 entries:
  "MA"  => 501×2 DataFrame…
  "src" => 501×2 DataFrame…
  "MD"  => 501×2 DataFrame

输出结果

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

左图显示的是输入到用户块Engee Function 的噪声信号。右图显示了移动平均值与滑动窗口方差值的关系。

可以看出,均值和方差的起点是零样本的值,但经过几十步后,这些参数的估计值转移到了$\mu \approx -0.15$,$\sigma \approx 0.55$ 点。在计算期结束时,当滑动窗口的初始化不再影响统计数据时,图上的点开始明显移向数学期望值为零的点$\sigma \approx 0.6$ 。

结论

如果在Engee Function 组件的原始代码模式中添加一些标准结构,就可以在该组件中体现非常复杂的行为,依赖于外部代码和可改变的参数。在Engee中,有许多图形和文字描述模型行为的方法,除此之外,您还可以使用动态块,在块中加入自定义代码。