Engee documentation
Notebook

Advanced settings of the Engee Function unit

The Engee Function block allows the user to embed their algorithm into the Engee model without redrawing it on primitives. The user has full control over the definition of the block's behaviour.

The following are examples of how this control can be exercised.

Overloading output parameters

Example: Conversion to a vector

You need to implement a block that converts its input to a vector.

No operations on the input are required. The only thing required is to convert the input into a vector. To do this, we need to calculate the length of the vector and get the type of its elements.

This task is implemented in the vectorize_it model by the VectorizeIT block.

image_3.png

Let's consider the code of this Engee Function. We are interested in the block's functor and the method of overriding dimensions and output type.

Let's start with the definition of the functor:

function (c::Block)(t::Real, cache, in1)
	if in1 isa AbstractVector
		cache .= in1
	elseif in1 isa AbstractArray
		cache .= vec(in1)
	elseif in1 isa Real
		cache[1] = in1
	end
end

Here we should pay attention to the cache variable. This variable is declared for the Engee Function block if the setting Use external cache for non-scalar output is enabled : image.png

This setting is only available if the block has only one output.

Next, let's look at defining the dimension and output type. You can define them separately by selecting the Override type inheritance method and Override dimensions inheritance method options. However, you can define them in a single function. To do this, select Use common method for types and dimensions inheritance (the selection will become available after both methods are enabled):

image_2.png

For this case, the latter option is most appropriate. Let's consider the code of this method:

function 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), )
	return ([eltype(in1t)], [outd]);
end

The input is two vectors: the types of the inputs and their dimensions. At the output, a tuple consisting of two vectors must be formed:

  • a vector of output signal data types
  • vector of output signal dimensions

It should be noted separately that the dimension is passed as a tuple (i.e. as the result of the function size()), and the data type as an element type. In our case it will not be Vector{eltype(in1t)}, but simply eltype(in1t).

Strict block typing and caching of results

Example: Calculating a system of equations

The Julia language allows you to dynamically allocate memory, but this results in slower code. Therefore, the Engee Function block can have a negative impact on the speed of the simulation. This example demonstrates how to maximise the performance of the Engee Function block.

Using a separate variable to store the result of the block's work (cache) allows you to get rid of dynamic memory allocation and significantly speeds up the block's work. Let's consider a simple example:

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

As you can see from the example, applying a cache is a highly desirable practice. For Engee Function, we can define the cache in two ways:

  1. By specifying the Use external cache for non-scalar output checkbox (for a single output)
  2. Manually creating caches in the block definition

To maximise performance, all fields in the block structure should have a specific type and a fixed size.

As an example, consider the cached_calcs model and the ComputingTypesRuntime block.

image.png

This block implements the following system of equations: $$ \begin{cases} y_1 = sin(u_1) + cos(u_2)\\ y_2 = u_1 + u_2 \end{cases} $$

The implementation of this system involves the use of caches since the inputs may be non-scalar.

To create caches we need to define a typed structure. The usual Define component structure method does not allow us to do this, but we can enable the Use common code setting and create a structure for a block in "common" code. The code for defining the block structure would be as follows:

struct Block{CacheType1,CacheType2} <: AbstractCausalComponent
	c1::CacheType1;
	c2::CacheType2;
  
	function 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);
	end
  end

It should be noted that the cache type and dimensions are derived from the outputs. And the attributes of the outputs are derived in the Use common method for types and dimensions inheritance using a single calculation of the system of equations:

function 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 = zeros(in1t, in1d)
	mockin2 = zeros(in2t, in2d)
	mockout1 = sin.(mockin1) .+ cos.(mockin2)
	mockout2 = mockin1 .+ mockin2
	return ([eltype(mockout1), eltype(mockout1)], [size(mockout1), size(mockout1)])
end

This technique will only result in a single memory allocation.

Direct feedthrough

Example: Breaking an algebraic loop

As an example, let's consider the practical task of breaking an algebraic loop. It is recommended to break an algebraic loop using a delay block. But this technique leads to distorted results. And the IC block does not break the loop.

Therefore, let's create such an Engee Function block that would implement the following system of equations and break the algebraic loop: $$ y(t) = \begin{cases} IC, t = 0\\ u, t > 0 \end{cases} $$ To break the algebraic loop, we need to remove the direct feedthrough attribute. This can be done either from the settings of the Engee Function block or programmatically by setting the Override direct feedthrough setting method option.

The example implementation is available in the loopbreaker model in the LoopBreaker block.

image.png

The principle of operation of the block is as follows: the parameter IC, which will be output at the moment of initialisation of the model, and at the initial moment of simulation time, is set. At the next steps of the simulation, the block input will be immediately passed to the output. Let's look at the implementation of this algorithm:

A block is defined as:

struct Block{T} <: AbstractCausalComponent
    InCn::T
    function Block()
        InCn_t = INPUT_SIGNAL_ATTRIBUTES[1].type;
        new{InCn_t}(IC)
    end
end

And its functor as:

function (c::Block)(t::Real, in1)
    if t<=0.0
        return c.InCn
    else
        return in1
    end
end

Additionally, the Override direct feedthrough setting method is defined. No logic or calculations are required here, so we just open the loop:

function direct_feedthroughs()::Vector{Bool}
    return [false]
end

Let's make sure that the simulation works correctly:

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)

Setting Sample Time

Example: "Slowing down" the output

There may be models and algorithms that require that the outputs of blocks be refreshed for a certain period of time. In the case of the Engee Function block, the user has two methods of implementing such a requirement:

  • explicitly specifying the sampling period through the block parameters
  • Writing their own algorithm to determine the required period

This example considers the second option.

Consider the sample_time_override model:

image.png

Functionally, the Engee Function blocks ST_Orig and ST_Override correspond to the algorithm from the Cache and strict typing section. However, there is now a requirement to produce the result with the lowest period. Let's see how the built-in sampling period inheritance mechanism works:

image_2.png

We can see that the period D1 is inherited, i.e. the highest period. But the task is to inherit D2. To do this, in the Engee Function code, let's enable the Override sample time inheritance method section, and choose the slowest period:

function propagate_sample_times(inputs_sample_times::Vector{SampleTime},
         fixed_solver::Bool)::SampleTime

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

end

After implementing this method the following result is obtained:

image_3.png

It can be seen that the desired period is now inherited, D2

IMPORTANT!

The first argument is a vector of named tuples of type SampleTime, given as:

SampleTime = NamedTuple{(:period, :offset, :mode), Tuple{Rational{Int64}, Rational{Int64}, Symbol}}

The mode field of this tuple takes the following values: :Discrete, :Continuous, :Constant, :FiM.

The second argument is a flag that indicates the class of the solver. If the flag is true, a constant step solver is selected.

Conclusions

Methods for advanced programming of the Engee Function block have been discussed. These methods allow you to define in detail the required block behaviour and achieve maximum performance. This material does not cover typical errors when writing block code and techniques of writing safe code.