Engee documentation
Notebook

Subsystems

The purpose of this demonstration is to show how to use all kinds of subsystems implemented in Engee. To do this, let's look at a few simple models.

In [ ]:
using Plots # Библиотека для отрисовки результатов моделирования

# Подключение вспомогательной функции запуска модели.
function run_model( name_model, path_to_folder )
    
    Path = path_to_folder * "/" * name_model * ".engee"
    
    if name_model in [m.name for m in engee.get_all_models()] # Проверка условия загрузки модели в ядро
        model = engee.open( name_model ) # Открыть модель
        model_output = engee.run( model, verbose=true ); # Запустить модель
    else
        model = engee.load( Path, force=true ) # Загрузить модель
        model_output = engee.run( model, verbose=true ); # Запустить модель
        engee.close( name_model, force=true ); # Закрыть модель
    end

    return model_output
end

# Путь до папки с моделями
path = "$(@__DIR__)/models"
Out[0]:
"/user/subsystems/models"

Enabled Subsystem

The first subsystem option that we will consider in this example is Enabled Subsystem. We are talking about a conditionally executed subsystem that is started once at each main time step, as long as the control signal has a positive value. If the signal crosses zero during a smaller time step, the subsystem does not turn on or off until the next major time step.

The control signal can be a scalar or a vector.

  1. If the scalar value is greater than zero, the subsystem is executed.
  2. If any of the values of the vector element is greater than zero, the subsystem is executed.

To use this functionality, add a block to the Subsystem block or to the root level of the model referenced by the Model block. If you are using Enable at the root level of the model, follow these steps.

  1. For multi-speed models, set the solver to single-task mode.
  2. For models with a fixed step size, at least one block in the model must operate at a specified speed of a fixed step size.

The picture below is an illustration of this example.

image.png

In this example, the control signal is a sine wave, and the input signal is a counter.

In [ ]:
run_model("enabled_subsystem",path) # Запуск модели.
Building...
Progress 0%
Progress 0%
Progress 5%
Progress 10%
Progress 15%
Progress 20%
Progress 25%
Progress 30%
Progress 35%
Progress 40%
Progress 45%
Progress 50%
Progress 55%
Progress 60%
Progress 65%
Progress 70%
Progress 75%
Progress 80%
Progress 85%
Progress 90%
Progress 95%
Progress 100%
Out[0]:
Dict{String, DataFrame} with 2 entries:
  "SubSystem.Out1" => 1001×2 DataFrame…
  "Sine Wave.1"    => 1001×2 DataFrame
In [ ]:
# Считывание из simout залогированных сигналов
data1 = simout["enabled_subsystem/SubSystem.Out1"];
data1 = collect(data1);
en1 = simout["enabled_subsystem/Sine Wave.1"];
en1 = collect(en1);

Let's plot the dependence of the output data on the control signal.

In [ ]:
plot(data1.time,data1.value) 
plot!(en1.time,en1.value) 
Out[0]:

As we can see from the graph, at those moments when the control signal drops below 0, the output signal fades.

Action Subsystem

Action Port controls the subsystem execution. The block adds an external input port for action signals to the subsystem. The action signal controls the signal connected to the Action port of the Subsystem block.

Used in models containing blocks:

  1. If
image.png
  1. Switch Case
image_3.png

Let's run these two models and analyze the results. Let's start with the cif model.

In [ ]:
run_model("if_subsystem",path) # Запуск модели.
Building...
Progress 0%
Progress 0%
Progress 5%
Progress 10%
Progress 15%
Progress 20%
Progress 25%
Progress 30%
Progress 35%
Progress 40%
Progress 45%
Progress 50%
Progress 55%
Progress 60%
Progress 65%
Progress 70%
Progress 75%
Progress 80%
Progress 85%
Progress 90%
Progress 95%
Progress 100%
Out[0]:
Dict{String, DataFrame} with 2 entries:
  "SysOutput_1" => 1001×2 DataFrame…
  "SysOutput_2" => 1001×2 DataFrame
In [ ]:
# Считывание из simout залогированных сигналов
SysOutput_1 = simout["if_subsystem/SysOutput_1"];
SysOutput_1 = collect(SysOutput_1);
SysOutput_2 = simout["if_subsystem/SysOutput_2"];
SysOutput_2 = collect(SysOutput_2);
In [ ]:
plot(SysOutput_1.time,SysOutput_1.value) 
plot!(SysOutput_2.time,SysOutput_2.value) 
Out[0]:

As we can see, in the case of if, the input sinusoid was divided into two signals: according to the condition greater than 0 and less than 0.

In [ ]:
run_model("switch_case_subsystem",path) # Запуск модели.
Building...
Progress 0%
Progress 0%
Progress 5%
Progress 10%
Progress 15%
Progress 20%
Progress 25%
Progress 30%
Progress 35%
Progress 40%
Progress 45%
Progress 50%
Progress 55%
Progress 60%
Progress 65%
Progress 70%
Progress 75%
Progress 80%
Progress 85%
Progress 90%
Progress 95%
Progress 100%
Out[0]:
Dict{String, DataFrame} with 3 entries:
  "SysOutput_1"       => 1001×2 DataFrame…
  "Pulse Generator.1" => 1001×2 DataFrame…
  "SysOutput_2"       => 1001×2 DataFrame
In [ ]:
# Считывание из simout залогированных сигналов
SysOutput_1 = simout["switch_case_subsystem/SysOutput_1"];
SysOutput_1 = collect(SysOutput_1);
SysOutput_2 = simout["switch_case_subsystem/SysOutput_2"];
SysOutput_2 = collect(SysOutput_2);
En = simout["switch_case_subsystem/Pulse Generator.1"];
En = collect(En);

Let's plot the dependence of the output signals on the control signal.

In [ ]:
plot(SysOutput_1.time,SysOutput_1.value) 
plot!(SysOutput_2.time,SysOutput_2.value)
plot!(En.time,En.value)
Out[0]:

As we can see, when the control signal is 1, the data is written from the upper subsystem. If the data is 0, then we get a graph of data from the second subsystem. Otherwise, the output signal on the subsystem freezes while waiting for the control signal.

Triggered Subsystem

Unlike the Enabled Subsystem block, the Triggered Subsystem block always keeps its output parameters at the last value between triggers. In addition, running subsystems cannot reset block states during execution; the states of any discrete block are saved between triggers.

The Trigger block adds an external signal to control subsystem execution. The subsystem will be executed once at each step when the value of the control signal is changed in the specified way. The block icon changes depending on the value selected for the Trigger type parameter. In addition, the Trigger block can be added to Enable. In this section, we will analyze both applications of the Triggered Subsystem.

To begin with, let's look at an example of a simple Triggered Subsystem. The implementation of the model using this approach is shown in the figure below.
image.png

In [ ]:
run_model("triggered_subsystem",path) # Запуск модели.
Building...
Progress 0%
Progress 0%
Progress 5%
Progress 10%
Progress 15%
Progress 20%
Progress 25%
Progress 30%
Progress 35%
Progress 40%
Progress 45%
Progress 50%
Progress 55%
Progress 60%
Progress 65%
Progress 70%
Progress 75%
Progress 80%
Progress 85%
Progress 90%
Progress 95%
Progress 100%
Out[0]:
Dict{String, DataFrame} with 2 entries:
  "SubSystem.Out1" => 1001×2 DataFrame…
  "Sine Wave-1.1"  => 1001×2 DataFrame
In [ ]:
# Считывание из simout залогированных сигналов
SysOutput_1 = simout["triggered_subsystem/SubSystem.Out1"];
SysOutput_1 = collect(SysOutput_1);
En = simout["triggered_subsystem/Sine Wave-1.1"];
En = collect(En);
In [ ]:
plot(SysOutput_1.time,SysOutput_1.value) 
plot!(En.time,En.value)
Out[0]:

According to the resulting data, we can see that such a subsystem is triggered only at the moment when the sinusoid 0 intersects.

Now let's consider the use of the Trigger and Enable blocks in the same subsystem. The figure below shows the model we implemented.

image_2.png
In [ ]:
run_model("triggered_enabled_subsystem",path) # Запуск модели.
Building...
Progress 0%
Progress 0%
Progress 5%
Progress 10%
Progress 15%
Progress 20%
Progress 25%
Progress 30%
Progress 35%
Progress 40%
Progress 45%
Progress 50%
Progress 55%
Progress 60%
Progress 65%
Progress 70%
Progress 75%
Progress 80%
Progress 85%
Progress 90%
Progress 95%
Progress 100%
Out[0]:
Dict{String, DataFrame} with 3 entries:
  "SubSystem.Out1" => 1001×2 DataFrame…
  "Enable.1"       => 1001×2 DataFrame…
  "Trigger.1"      => 1001×2 DataFrame
In [ ]:
# Считывание из simout залогированных сигналов
Data = simout["triggered_enabled_subsystem/SubSystem.Out1"];
Data = collect(Data);
Trigger = simout["triggered_enabled_subsystem/Trigger.1"];
Trigger = collect(Trigger);
Enable = simout["triggered_enabled_subsystem/Enable.1"];
Enable = collect(Enable);

Let's analyze the output relative to the two control signals.

In [ ]:
plot(Data.time,Data.value) 
plot!(Trigger.time,Trigger.value)
plot!(Enable.time,Enable.value)
Out[0]:

As we can see, the frequency of the Enable signal is three times higher than the frequency of the Trigger, so these two signals intersected only once and thus started the subsystem.

For Each Subsystem

The For Each block is a control block for For Each Subsystem. This subsystem allows you to process parts of the input signals.

Each block within this subsystem maintains a separate set of states for each element or subarray that it processes. As a set of subsystem blocks processes elements or subarrays, the subsystem combines the results to form output signals.

The figures below show the model in which the For Each block is used.

image.png image_3.png
In [ ]:
run_model("for_each_model1",path) # Запуск модели.
Building...
Progress 10%
Progress 20%
Progress 30%
Progress 40%
Progress 50%
Progress 60%
Progress 70%
Progress 80%
Progress 90%
Progress 100%
Progress 100%
Out[0]:
Dict{String, DataFrame} with 2 entries:
  "SysOutput_1" => 11×2 DataFrame…
  "Constant.1"  => 11×2 DataFrame
In [ ]:
# Считывание из simout залогированных сигналов
Data_in = simout["for_each_model1/Constant.1"];
Data_in = collect(Data_in);
Data_out = simout["for_each_model1/SysOutput_1"];
Data_out = collect(Data_out);
print(string(Data_in.value[1]) *" --> " * string(Data_out.value[1]))
[1.0, 2.0, 3.0] --> [2.0, 4.0, 6.0]

As we can see, the result was a piecemeal multiplication of the input data by 2.

This block also allows you to create indexes for input data and output them as a port. The figures below show such an application For Each.
image.png

image_2.png

The counter settings also indicate that the number of iterations is 3.

image_4.png
In [ ]:
run_model("for_each_model2",path) # Запуск модели
Building...
Progress 10%
Progress 20%
Progress 30%
Progress 40%
Progress 50%
Progress 60%
Progress 70%
Progress 80%
Progress 90%
Progress 100%
Progress 100%
Out[0]:
Dict{String, DataFrame} with 2 entries:
  "SysOutput_1"   => 11×2 DataFrame…
  "SubSystem.idx" => 11×2 DataFrame
In [ ]:
# Считывание из simout залогированных сигналов
Data_out = simout["for_each_model2/SysOutput_1"];
Data_out = collect(Data_out);
idx = simout["for_each_model2/SubSystem.idx"];
idx = collect(idx);
In [ ]:
Data_out.value[1]
Out[0]:
2×6 Matrix{Float64}:
 0.0  0.0  1.0  1.0  2.0  2.0
 0.0  0.0  1.0  1.0  2.0  2.0
In [ ]:
print(string(idx.value[1]))
[0.0, 1.0, 2.0]

As we can see, in this case, the input unit matrix was multiplied by the counter port, taking into account the fact that there are only three iterations of processing input data.

Function-Call Subsystems

The Function-Call Subsystem block is a conditionally executed subsystem that is started every time the control port receives a function call event. Chart, the Function-Call Generator block, or the Engee Function block can provide function call events.

The subsystem for calling functions is similar to a function in a procedural programming language. Calling the function call subsystem triggers the block output methods within the subsystem in the order of execution. The figures below show an example of using a Function-Call Generator and Function-Call Subsystems.

image.png image_2.png
In [ ]:
run_model("function_call",path) # Запуск модели.
Building...
Progress 100%
Out[0]:
Dict{String, DataFrames.DataFrame} with 2 entries:
  "SysOutput_1" => 1001×2 DataFrame…
  "Sine Wave.1" => 1001×2 DataFrame
In [ ]:
# Считывание из simout залогированных сигналов
out = simout["function_call/SysOutput_1"];
out = collect(out);
inp = simout["function_call/Sine Wave.1"];
inp = collect(inp);

The input sinusoid.

In [ ]:
plot(inp.time,inp.value) 
Out[0]:

The output amount.

In [ ]:
plot(out.time,out.value) 
Out[0]:

As we can see in this example, according to the condition of the control function, the counter increment is performed inside the subsystem.

Conclusion

In this demo, we have analyzed all the Subsystems options that you can find in Engee and use in your projects.