Engee 功能单元的高级设置
Engee 功能块允许用户将其算法嵌入 Engee 模型,而无需在基元上重新绘制。用户可以完全控制功能块的行为定义。
下面举例说明如何行使这种控制权。
重载输出参数
示例转换为向量
您需要实现一个将输入转换为矢量的程序块。
不需要对输入进行任何操作。唯一需要做的就是将输入转换为矢量。为此,我们需要计算向量的长度并获取其元素的类型。
这项任务在 vectorize_it 模型中由 VectorizeIT 模块实现。

让我们来看看 Engee 函数的代码。我们感兴趣的是程序块的函数以及覆盖维数和输出类型的方法。
让我们从函数的定义开始:
``朱莉娅
function (c::Block)(t::Real, cache, in1)
如果 in1 是抽象矢量
cache .= in1
elseif in1 是抽象数组
cache .= vec(in1)
elseif in1 isa Real
cache[1] = in1
结束
结束
这里我们需要注意 cache 变量。如果启用`Use external cache for non-scalar output` 设置,则为 Engee 功能块声明该变量:
<img src="/helpcenter/stable/_next/static/media/example_resources/EF_adv_programming_media/image.png" alt="image.png" class="notebook-media">
只有当程序块只有一个***输出时,该设置才有效。
接下来,我们来定义维度和输出类型。您可以通过选择_覆盖类型继承方法_和_覆盖尺寸继承方法_来分别定义它们。不过,您也可以在一个函数中定义它们。为此,请选择 _Use common method for types and dimensions inheritance_(类型和维度继承使用通用方法)(启用这两种方法后,该选项将变为可用):
<img src="/helpcenter/stable/_next/static/media/example_resources/EF_adv_programming_media/image_2.png" alt="image_2.png" class="notebook-media">
在本例中,后一个选项是最合适的。让我们来看看该方法的代码:
``朱莉娅
function propagate_types_and_dimensions(inputs_types::Vector{DataType}, inputs_dimensions::Vector{<:Dimensions})::Tuple{Vector{DataType}, Vector{<:Dimensions}}
in1t = 输入类型[1]
in1d = 输入尺寸[1]
outd = (prod(in1d), )
返回 ([eltype(in1t)], [outd]);
结束
输入是两个向量:输入的类型及其维数。在输出端,必须形成一个由两个向量组成的元组:
- 输出信号数据类型向量
- 输出信号尺寸向量
需要单独指出的是,维度是作为一个元组(即函数size()
的结果)传递的,而数据类型是作为一个元素类型传递的。在我们的例子中,它不是Vector{eltype(in1t)}
,而只是eltype(in1t)
。
严格的块类型和结果缓存
示例计算方程组
Julia 语言允许动态分配内存,但这会导致代码速度变慢。因此,Engee 功能块会对仿真速度产生负面影响。本例演示了如何最大限度地提高 Engee 功能块的性能。
使用一个单独的变量来存储程序块的工作结果(缓存),可以摆脱动态内存分配,大大加快程序块的工作速度。我们来看一个简单的例子:
a = [[i, 2i, 3i] for i in 1:1_000_000]
b = [[4i, 5i, 6i] for i in 1:1_000_000]
cache = [0, 0, 0]
function sum_without_cache(a, b)
@time for i in eachindex(a)
a[i] .+ b[i]
end
end
function sum_with_cache(cache, a, b)
@time for i in eachindex(a)
cache .= a[i] .+ b[i]
end
end
println("Without cache:")
sum_without_cache(a, b)
println("With cache:")
sum_with_cache(cache, a, b);
从示例中可以看出,应用缓存是一种非常可取的做法。对于 Engee Function,我们可以通过两种方式定义缓存:
1.指定 Use external cache for non-scalar output 复选框(用于单一输出); 2.
2.在程序块定义中手动创建缓存
为最大限度地提高性能,块结构中的所有字段都应具有特定类型和固定大小。
举例来说,cached_calcs 模型和ComputingTypesRuntime 块。

该程序块实现了以下方程组:
由于输入可能是非标量的,因此该系统的实现需要使用缓存。
为了创建缓存,我们需要定义一个类型化结构。通常的定义组件结构方法不允许我们这样做,但我们可以启用使用普通代码设置,并在 "普通 "代码中为程序块创建结构。定义程序块结构的代码如下:
``朱莉娅
struct Block{CacheType1,CacheType2} <: AbstractCausalComponent
c1::CacheType1;
c2::CacheType2;
函数 Block()
sz1 = OUTPUT_SIGNAL_ATTRIBUTES[1].dimensions;
sz2 = OUTPUT_SIGNAL_ATTRIBUTES[2].dimensions;
type1 = OUTPUT_SIGNAL_ATTRIBUTES[1].type;
type2 = OUTPUT_SIGNAL_ATTRIBUTES[2].type;
c1 = isempty(d1) ?zero(t1) :zeros(t1, d1)
c2 = isempty(d2) ?zero(t2) : zeros(t2, d2)
new{typeof(c1), typeof(c2)}(c1,c2);
结束
结束
需要注意的是,缓存类型和维度是从输出中派生出来的。而输出的属性是在_Use common method for types and dimensions inheritance_(使用类型和维度继承的常用方法)中通过方程组的单一计算得出的:
``朱利亚
function propagate_types_and_dimensions(inputs_types::Vector{DataType}、
inputs_dimensions::Vector{<:Dimensions})
::Tuple{Vector{DataType}, Vector{<:Dimensions}}
in1t = 输入类型[1]
in2t = 输入类型[2]
in1d = 输入维数[1]
in2d = 输入维度[2]
mockin1 = zeros(in1t, in1d)
mockin2 = zeros(in2t, in2d)
mockout1 = sin.(mockin1) .+ cos.(mockin2)
mockout2 = mockin1 .+ mockin2
return ([eltype(mockout1), eltype(mockout1)], [size(mockout1), size(mockout1)])
结束
这种方法只会导致一次内存分配。
直接穿通
举例说明:打破代数循环
作为一个例子,让我们考虑一下打破代数循环的实际任务。建议使用延迟块打破代数循环。但这种方法会导致结果失真。而且 IC 块并不能打破循环。
因此,让我们创建这样一个 Engee 功能块来实现下面的方程组并打破代数循环:
为了打破代数循环,我们需要删除直接穿通属性。这可以通过 Engee 功能块的设置来实现,也可以通过设置_覆盖直接穿通设置方法_选项来编程实现。
示例实现可在LoopBreaker块中的loopbreaker模型中找到。

程序块的工作原理如下:设置参数IC
,该参数将在模型初始化时和模拟时间的初始时刻输出。在接下来的仿真步骤中,程序块的输入将立即传递到输出。让我们来看看这种算法的实现:
区块的定义如下
``朱莉娅
struct Block{T} <: AbstractCausalComponent
InCn::T
函数 Block()
InCn_t = INPUT_SIGNAL_ATTRIBUTES[1].type;
newIC
结束
结束
其函数为
```julia
函数 (c::Block)(t::Real, in1)
如果 t<=0.0
返回 c.InCn
否则
返回 in1
结束
结束
此外,还定义了_覆盖直接穿通设置方法_。这里不需要逻辑或计算,因此我们只需打开循环:
``朱莉娅
function direct_feedthroughs()::Vector{Bool} 返回
return [false] 返回 [false
结束
让我们确保模拟工作正常:
demoroot = @__DIR__
mdl = engee.load(joinpath((demoroot),"loopbreaker.engee");force=true);
simres = engee.run(mdl)
st = collect(simres["Ref.1"])
res = collect(simres["LoopBreaker.1"])
using Plots
p = Plots.plot(st.time, st.value, label="Step")
Plots.plot!(res.time, res.value, label="LoopBreaker")
Plots.plot!(legend=:topleft)
engee.close(mdl;force=true)
display(p)
设置采样时间
示例"减慢 "输出
有些模型和算法可能需要在一定时间内刷新程序块的输出。对于 Engee 功能块,用户有两种方法来实现这种要求:
- 通过程序块参数明确指定采样周期
- 编写自己的算法来确定所需的周期
本示例采用第二种方法。
考虑 sample_time_override 模型:

在功能上,Engee 功能块 ST_Orig 和 ST_Override 与高速缓存和严格类型部分的算法相对应。不过,现在的要求是产生周期最小的结果。让我们看看内置的采样周期继承机制是如何工作的:

我们可以看到,周期 D1 已被继承,即最高周期。但我们的任务是继承 D2。为此,让我们在 Engee 函数代码中启用 "覆盖采样时间继承方法 "部分,并选择最慢的周期:
``朱莉娅
function propagate_sample_times(inputs_sample_times::Vector{SampleTime}、
fixed_solver::Bool)::SampleTime
max_period = argmax(t -> t.period, inputs_sample_times)
返回 max_period
结束
执行此方法后,结果如下
<img src="/helpcenter/stable/_next/static/media/example_resources/EF_adv_programming_media/image_3.png" alt="image_3.png" class="notebook-media">
可以看出,现在继承了所需的周期,D2
#### 重要!
第一个参数是一个 SampleTime 类型的命名元组向量,格式为
``朱莉娅
SampleTime = NamedTuple{(:period, :offset, :mode), Tuple{Rational{Int64}, Rational{Int64}, Symbol}}
此元组的模式字段取以下值::离散、:连续、:常量、:FiM。
第二个参数是一个标志,表示求解器的类别。如果标志为 true,则选择恒步求解器。
结论
本文讨论了 Engee 功能块的高级编程方法。通过这些方法,您可以详细定义所需的程序块行为,并实现最高性能。本资料不包括编写块代码时的典型错误和编写安全代码的技巧。