Engee 文档
Notebook

Engee功能块的高级设置

Engee功能块允许用户将他们的算法嵌入到Engee模型中,而无需在基元上重绘它。 与此同时,用户可以完全控制确定块的行为。

这样的控制方法的例子在下面给出。

输出参数过载

示例:转换为矢量

需要实现将其输入转换为矢量的块。

不需要登录操作。 唯一需要的是将输入转换为矢量。 为此,计算向量的长度并获取其元素的类型。

此任务由VectorizeIT块在vectorize_it模型中实现。

image_3.png

考虑这个Engee函数的代码。 我们对块函子和重新定义输出的维度和类型的方法感兴趣。

让我们从函子的定义开始:

``'茱莉亚
函数(c::块)(T::Real,缓存,in1)
如果in1isa AbstractVector
缓存。=in1
elseif in1isa抽象阵列
缓存。=vec(in1)
elseif in1是真实的
缓存[1]=in1
结束
结束


这里要注意cache变量。 如果启用该设置,则为Engee功能块声明此变量。 `Use external cache for non-scalar output`:
<img src="/helpcenter/stable/_next/static/media/example_resources/EF_adv_programming_media/image.png" alt="image.png" class="notebook-media">

只有当块只有**一个**输出时,此设置才可用。

接下来,我们将考虑输出信号的维度和类型的定义。 您可以通过选择_Override类型继承method_和_Override维度继承method_选项来单独定义它们。 但是,可以在单个函数中定义它们。 为此,请选择_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">

后一种选择最适合这种情况。 考虑此方法的代码:

``'茱莉亚
函数propagate_types_and_dimensions(inputs_types::Vector{DataType},inputs_dimensions::Vector{<:Dimensions})::Tuple{Vector{DataType},Vector{<:Dimensions}}
	in1t=inputs_types[1]
	in1d=inputs_dimensions[1]
	outd=(prod(in1d),)
	返回([eltype(in1t)],[outd]);
结束

输入接收两个向量:输入类型及其维度。 应在输出端形成由两个向量组成的元组:
*输出信号的数据类型的向量
*输出信号维数向量

应该单独注意的是,维度作为元组传递(即,以及函数的结果 size()),并将数据类型作为元素类型。 在我们的情况下,它不会是 Vector{eltype(in1t)},而只是 eltype(in1t).

严格的块类型和缓存结果

示例:方程组的计算

Julia语言允许您动态分配内存,但这会减慢代码的速度。 因此,Engee功能块可以对仿真速度产生负面影响。 此示例演示如何最大限度地提高Engee功能块的性能。

使用单独的变量来存储块操作的结果(缓存)可以让您摆脱动态内存分配并显着加快块操作。 让我们举一个简单的例子:

In [ ]:
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);
Without cache:
  1.056730 seconds (1000.00 k allocations: 76.294 MiB, 90.59% gc time)
With cache:
  0.023816 seconds

从示例中可以看出,使用缓存是一种非常理想的做法。 对于Engee函数,我们可以通过两种方式定义缓存:

  1. 通过为非标量output_指定复选框_Use external cache(对于一个输出)
  2. 在块定义中手动创建缓存

为了获得最大的性能,块结构中的所有字段都必须具有特定的类型和固定的大小。

例如,考虑cached_calcs模型和ComputingTypesRuntime块。

image.png

此块实现以下方程组:

该系统的实现涉及使用缓存,因为输入可以是非标量的。

要创建缓存,我们需要定义一个类型化结构。 通常的Define component structure方法不允许这样做,但我们可以启用Use common code设置,并在"common"代码中为块创建结构。 块结构定义代码将如下所示:

``'茱莉亚
struct Block{CacheType1,CacheType2}<:AbstractCausalComponent
c1::CacheType1;
c2::CacheType2;

功能块()
  sz1=OUTPUT_SIGNAL_ATTRIBUTES[1]。尺寸;
  sz2=OUTPUT_SIGNAL_ATTRIBUTES[2]。尺寸;
  type1=OUTPUT_SIGNAL_ATTRIBUTES[1]。类型;
  type2=OUTPUT_SIGNAL_ATTRIBUTES[2]。类型;

  c1=isempty(d1)? 零(t1):零(t1,d1)
  c2=isempty(d2)? 零(t2):零(t2,d2)

  new{typeof(c1),typeof(c2)}(c1,c2);
结束

结束


应该注意的是,缓存的类型和维度是从输出信号中派生出来的。 并且输出信号的属性在_use common method for types and dimensions inheritance_中输出,使用方程组的单个计算:

``'茱莉亚
函数propagate_types_and_dimensions(inputs_types::Vector{DataType},
 			inputs_dimensions::Vector{<:Dimensions})
			::Tuple{Vector{DataType},Vector{<:Dimensions}}
	in1t=inputs_types[1]
	in2t=inputs_types[2]
	in1d=inputs_dimensions[1]
	in2d=inputs_dimensions[2]
	mockin1=零(in1t,in1d)
	mockin2=零(in2t,in2d)
	mockout1=罪。(mockin1)。+cos.(mockin2)
	mockout2=mockin1。+mockin2
	return([eltype(mockout1),eltype(mockout1)],[size(mockout1),size(mockout1)])
结束

这种技术只会导致单个内存分配。

直接馈通

示例:打破代数循环

作为一个例子,让我们考虑打破代数循环的实际问题。 建议使用延迟块打破代数循环。 但这种技术导致结果的失真。 和IC单元不破环。

因此,我们将创建一个Engee功能块,实现以下方程组并打破代数循环:

要打破代数循环,我们需要删除直接馈通属性。 这既可以从Engee功能块的设置中完成,也可以通过设置_Override直接馈通设置method_选项以编程方式完成。

示例实现在LoopBreaker块中的loopbreaker模型中可用。

image.png

块的操作原理如下:参数设置 IC,其将在模型的初始化时输出,并在仿真的初始时输出。 在仿真的后续步骤中,块的输入将立即传输到输出。 让我们来看看这个算法的实现。:

块定义为:

``'茱莉亚
struct Block{T}<:AbstractCausalComponent
InCn::T
功能块()
InCn_t=INPUT_SIGNAL_ATTRIBUTES[1]。类型;
IC
结束
结束

它的函子就像:

``'茱莉亚
函数(c::块)(t::实,in1)
    如果t<=0.0
        返回C.InCn
    其他
        返回1
    结束
结束

此外,还定义了_Override直接馈通设置method_。 这里不需要逻辑或计算,所以我们只是打开循环。:

``'茱莉亚
函数direct_feedthroughs()::矢量{Bool}
返回[错误]
结束


让我们确保模拟工作正常。:


In [ ]:
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模型:

image.png

在功能上,Engee函数ST_OrigST_Override块对应于来自缓存和强类型部分的算法。 然而,现在有一个要求,以最低的周期产生的结果。 让我们来看看采样周期的内置继承机制是如何工作的。:

image_2.png

可以看出,周期D1是继承的,即最高周期。 但挑战在于继承D2。 为此,我们将在Engee函数代码中包含复盖示例时间继承方法部分,并选择最慢的周期。:

``'茱莉亚
函数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}}

此元组的mode字段采用以下值::Discrete,:Continuous,:Constant,:Fim

第二个参数是显示求解器类的标志。 如果标志为true,则选择恒定步长求解器。

结论

考虑了Engee功能块高级编程的方法. 这些方法允许您详细确定所需的块行为并实现最大性能。 本文不介绍编写块代码时的典型错误以及编写安全代码的技术。

示例中使用的块