Engee 文档
Notebook

Engee 功能单元的高级设置

Engee 功能块允许用户将其算法嵌入 Engee 模型,而无需在基元上重新绘制。用户可以完全控制功能块的行为定义。

下面举例说明如何行使这种控制权。

重载输出参数

示例转换为向量

您需要实现一个将输入转换为矢量的程序块。

不需要对输入进行任何操作。唯一需要做的就是将输入转换为矢量。为此,我们需要计算向量的长度并获取其元素的类型。

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

image_3.png

让我们来看看 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 功能块声明该变量:
![image.png](attachment:image.png)

只有当程序块只有一个***输出时,该设置才有效。

接下来,我们来定义维度和输出类型。您可以通过选择_覆盖类型继承方法_和_覆盖尺寸继承方法_来分别定义它们。不过,您也可以在一个函数中定义它们。为此,请选择 _Use common method for types and dimensions inheritance_(类型和维度继承使用通用方法)(启用这两种方法后,该选项将变为可用): 

![image_2.png](attachment:image_2.png)

在本例中,后一个选项是最合适的。让我们来看看该方法的代码:

``朱莉娅
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 功能块的性能。

使用一个单独的变量来存储程序块的工作结果(缓存),可以摆脱动态内存分配,大大加快程序块的工作速度。我们来看一个简单的例子:

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 Function,我们可以通过两种方式定义缓存:

1.指定 Use external cache for non-scalar output 复选框(用于单一输出); 2. 2.在程序块定义中手动创建缓存

为最大限度地提高性能,块结构中的所有字段都应具有特定类型和固定大小。

举例来说,cached_calcs 模型和ComputingTypesRuntime 块。

image.png

该程序块实现了以下方程组: $$ \begin{cases} y_1 = sin(u_1) + cos(u_2)\\ y_2 = u_1 + u_2 \end{cases} $$

由于输入可能是非标量的,因此该系统的实现需要使用缓存。

为了创建缓存,我们需要定义一个类型化结构。通常的定义组件结构方法不允许我们这样做,但我们可以启用使用普通代码设置,并在 "普通 "代码中为程序块创建结构。定义程序块结构的代码如下:

``朱莉娅 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 功能块来实现下面的方程组并打破代数循环: $$ y(t) = \begin{cases} IC, t = 0\\ u, t > 0 \end{cases} $$ 为了打破代数循环,我们需要删除直接穿通属性。这可以通过 Engee 功能块的设置来实现,也可以通过设置_覆盖直接穿通设置方法_选项来编程实现。

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

image.png

程序块的工作原理如下:设置参数IC ,该参数将在模型初始化时和模拟时间的初始时刻输出。在接下来的仿真步骤中,程序块的输入将立即传递到输出。让我们来看看这种算法的实现:

区块的定义如下

``朱莉娅 struct Block{T} <: AbstractCausalComponent InCn::T 函数 Block() InCn_t = INPUT_SIGNAL_ATTRIBUTES[1].type; new{InCn_t}(IC) 结束 结束

其函数为

```julia
函数 (c::Block)(t::Real, in1)
    如果 t<=0.0
        返回 c.InCn
    否则
        返回 in1
    结束
结束

此外,还定义了_覆盖直接穿通设置方法_。这里不需要逻辑或计算,因此我们只需打开循环:

``朱莉娅 function direct_feedthroughs()::Vector{Bool} 返回 return [false] 返回 [false 结束


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


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 函数代码中启用 "覆盖采样时间继承方法 "部分,并选择最慢的周期:

``朱莉娅 function propagate_sample_times(inputs_sample_times::Vector{SampleTime}、 fixed_solver::Bool)::SampleTime

max_period = argmax(t -> t.period, inputs_sample_times)
返回 max_period

结束


执行此方法后,结果如下

![image_3.png](attachment:image_3.png)

可以看出,现在继承了所需的周期,D2

#### 重要! 

第一个参数是一个 SampleTime 类型的命名元组向量,格式为
``朱莉娅
SampleTime = NamedTuple{(:period, :offset, :mode), Tuple{Rational{Int64}, Rational{Int64}, Symbol}}

此元组的模式字段取以下值::离散:连续:常量:FiM

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

结论

本文讨论了 Engee 功能块的高级编程方法。通过这些方法,您可以详细定义所需的程序块行为,并实现最高性能。本资料不包括编写块代码时的典型错误和编写安全代码的技巧。