Engee documentation

Engee Function

Usage of Julia code in models.

engee function

Description

Block Engee Function allows the use of Julia language code in Engee models.

For more information about the Julia programming language, see Programming.
In the block Engee Function it is allowed to use most of the features of the Julia language. However, usage of the Pkg package manager is not allowed.

Usage

To integrate Julia code into the Engee model you need to:

  1. Add a block to the model Engee Function from the Basic/User Functions section of the block library blocks library icon;

  2. In settings window debug article icon 1 on the Main tab of the block Engee Function click on the Edit Source Code button to open the source code editor (EngeeFunctionCode):

engee function code editor

Source code cells

The EngeeFunctionCode source code editor consists of function cells with Julia code. By default, three cells are available: auxiliary (uneditable), Component struct code и Step method code (the cells can be hidden):

engee function all start cell

To connect additional source code files, you can use the include() function in the cell Common code (see below for a description of the cell):

include("/user/engeefunction/source.jl")
All the code for the block Engee Function can be written in a cell *Common code*You have full control over the component structure, signatures and number of functions.

To add/remove other function cells, click on the "Manage Methods" button engee function all methods and check/uncheck the required cells:

engee function сhoose methods -> engee function сhoose methods 1

Each cell is responsible for a unique block functionality Engee Function. Let’s take a closer look at them:

  • Information cell (non-editable) - automatically displays block variables *Engee Function*attributes of input and output signals (dimension, type, discreteness) and other parameters set by the user. Its content is updated dynamically depending on the block settings. The cell is always active, but is not selectable in the method control menu engee function all methods. It has semi-transparent text, which is used to display tooltips.

    engee function start cell

    Changing the block parameters affects not only the contents of the information cell, but also the tooltips in other cells, which are also displayed in semi-transparent text.
  • Define component struct - adds the cell Component struct code*which specifies the structure of the block component *Engee Function (inherited from AbstractCausalComponent type). The structure fields are defined between the uneditable lines struct Block <: AbstractCausalComponent and end. By default, a parameter g is created and initialised with the block value gain, and the Block() constructor takes no arguments.

    engee function component struct code

  • Use common code - adds a cell Common code*in which the code is written in free form. By default the cell is empty. For example, if the standard structure declaration in *Component struct code is not suitable (because of the uneditable struct Block <: AbstractCausalComponent), you can disable Define component struct to remove the cell, and define the component structure manually in the Common code. The same goes for any functions from the engee function all methods method control menu - instead of standard cells, you can write your own code in . Common code.

    engee function common code

    The declaration of component and functor is mandatory for operation Engee Function. If Define component struct and Define step method are disabled, their code must be set to Common code, otherwise the block will not work.
    To override inheritance functions (Override, see below), you must first switch on the corresponding cell, erase its contents, and then write new code in the Common code.
  • Define step method - adds a cell Step method code*which defines the Step method that calculates output signals of the block *Engee Function. The method signature is generated automatically depending on the values of the corresponding labels of ports in the Ports tab. The method is represented as a functor (see here for details) and is called at each simulation step. The fields are defined between the uneditable strings function (c::Block)(t::Real, in1) and end. The first argument of t::Real is the simulation time, then the input variables are passed in. The calculated values of the outputs are returned via the return keyword.

    engee function define step method

  • Define update method - adds a Update method code cell, in which the update! method updates the internal state of the block Engee Function at each step of the simulation. The first argument c::Block is the block structure, the second t::Real is the simulation time, then the input signals are passed. If the block has no internal state, the method can remain empty and just return c.

    engee function update method code

    If you want to define multiple update! methods or define a method with a different signature, the Update method code cell can be disabled and write the code in the cell Common code. The compiler will automatically detect the presence of the update! method and use it for simulation.
  • Define terminate method code - adds a Terminate method code cell that is executed when the block simulation is terminated Engee Function (using the terminate! method). The first argument of c::Block is the structure of the block. By default, the method does not perform any additional actions and simply returns c.

    engee function terminate method code

  • Override type inheritance method - adds a Types inheritance method cell that overrides the type inheritance method.

    For more information about type inheritance method

    Specifies the type inheritance method:

    • If unchecked (by default) - input/output port types are inherited according to the rules specified in the input/output port description.

    • If the checkbox is checked - input/output port types are inherited according to the rules specified in the propagate_types function of the Types inheritance method cell in the source code.

      • The propagate_types function takes one argument - a vector of types, one type for each input signal type and returns a vector of output types.

    By default cell code:

    function propagate_types(inputs_types::Vector{DataType})::Vector{DataType}
    # Функция, возвращающая массив типов сигналов на выходе.
    # Игнорируется, если используется алгоритм по умолчанию.
    # В данном случае учитываются тип входного сигнала и тип
    # параметра блока `gain`.
        input_type = first(inputs_types)
        # promote_type возвращает тип, к которому приводятся типы-аргументы
        # при арифметических операциях с объектами этих типов.
        output_type = promote_type(input_type, eltype(gain))
        return [output_type]
    end

    Here for type inheritance the common element of the input signal and the element of the parameters set in the block settings in section Setting parameters.

  • Override dimensions inheritance method - adds the Dimensions inheritance method cell, which overrides the method of dimensions inheritance.

    For more information about dimensions inheritance method.

    Specifies the method of dimensions inheritance:

    • If unchecked (by default) - input/output port dimensions are inherited according to the rules specified in the input/output port description.

    • If the checkbox is checked - input/output port dimensions are inherited according to the rules specified in the propagate_dimensions function of the Dimensions inheritance method cell in the source code.

      • The propagate_dimensions function accepts an array of tuples (dimensions) on each input signal and returns an array of dimensions on the output signal.

    By default cell code:

    function propagate_dimensions(inputs_dimensions::Vector{<:Dimensions})::Vector{<:Dimensions}
    # Функция, возвращающая массив размерностей сигналов на выходе.
    # Игнорируется, если используется алгоритм по умолчанию.
    # В данном случае учитываются размерности входного сигнала и
    # параметра блока `gain`.
        input_dimensions = first(inputs_dimensions)
        mock_input = zeros(input_dimensions)
        mock_output = mock_input .* gain
        return [size(mock_output)]
    end

    Here, for dimension inheritance, an array with the required dimensions (mock_input) consisting of zeros is taken and multiplied by the element of the parameter set in the block parameters in section Setting parametersand then its dimension is taken.

  • Use common method for types and dimensions inheritance - adds the Common types and dimensions inheritance method cell, which uses a common method to override inheritance of types and dimensions at the same time.

    For more information about types and dimensions inheritance method.

    Unlike specific methods for types (Types inheritance method) or dimensions (Dimensions inheritance method), the generic method includes both types and dimensions at the same time:

    • If the checkbox is unchecked (by default) - general method is ignored, dimensions and input/output port types are inherited according to the rules specified in the input/output port description or in the Override type inheritance method (if enabled) and Override dimensions inheritance method (if enabled).

    • If the checkbox is enabled - dimensions and types of input/output ports are inherited according to the rules specified in the propagate_types_and_dimensions function of the Common types and dimensions inheritance method cell in the source code.

    By default cell code:

    function propagate_types_and_dimensions(inputs_types::Vector{DataType}, inputs_dimensions::Vector{<:Dimensions})::Tuple{Vector{DataType}, Vector{<:Dimensions}}
    # Функция, возвращающая массив типов сигналов и массив
    # размерностей сигналов на выходе. Эту функцию можно использовать
    # если необходимо одновременно переопределить и алгоритм наследования
    # типов сигналов, и алгоритм наследования размерностей.
        outputs_types = propagate_types(inputs_types)
        outputs_dimensions = propagate_dimensions(inputs_dimensions)
        return outputs_types, outputs_dimensions
    end

    Dependencies

    To use this cell, select the Override type inheritance method and Override dimensions inheritance method checkboxes.

  • Override sample time inheritance method - adds a Sample times inheritance method cell that overrides the calculation step inheritance method.

    For more information about the calculation step inheritance method.
    The code for the SampleTime calculation step structure and propagate_sample_times function will not be automatically added to the EngeeFunctionCode source code of old Engee models. To finalise old models, add the calculation step structure and function yourself.

    Specifies the inheritance method of the calculation step:

    • If unchecked (by default) - the preset calculation step inheritance method (by default Default) from the Sample time inheritance method parameters of the Advanced tab is used. For more information about preset methods, see below.

    • If the checkbox is checked - the preset methods of the Advanced tab are ignored (parameters are unavailable), the independent method from the Sample times inheritance method cell in the EngeeFunctionCode source code is used.

    For the independent method to work, it is necessary to find the propagate_sample_times function line and manually set the required calculation step.

    By default cell code:

    function propagate_sample_times(inputs_sample_times::Vector{SampleTime}, fixed_solver::Bool)::SampleTime
    # Функция, возвращающая время дискретизации блока.
    # Используется только в режиме наследования `Custom`.
    # Параметр fixed_solver говорит о том, используется решатель
    # с постоянным шагом (true) или с переменным (false).
    # Более сложный пример работы с наследованием времени
    # дискретизации блока можно посмотреть в документации.
        return first(inputs_sample_times)
    end

    where the calculation step has a structure:

    const SampleTime = NamedTuple{(:period, :offset, :mode), Tuple{Rational{Int64}, Rational{Int64}, Symbol}}
  • Override direct feedthrough setting method - adds a Direct feedthrough setting method cell that defines a direct feedthrough connection.

    For more information on defining a direct feedthrough connection

    Defines a direct feedthrough connection:

    • If unchecked (by default), direct through connection is not available. This means that the output signal will not be monitored by the input port value and allows the unit to open loops.

    • If the checkbox is selected, then a direct pass-through connection is available. This means that the output signal is monitored directly by the input port value.

    By default cell code:

    function direct_feedthroughs()::Vector{Bool}
    # Функция, возвращающая массив булевых значений, определяющих,
    # сквозные соединения. Если i-ый элемент массива равен true,
    # то i-ый порт имеет сквозное соединение.
    # Игнорируется, если используется алгоритм по умолчанию.
        return [true]
    end

    Example:

    function direct_feedthroughs()::Vector{Bool}
        if gain == 2
            return [false]
        else
            return [true]
        end
    end

Constants and functions for obtaining attributes

To find out types, sizes, and other auxiliary information in the executable code of a block, use the following constants within your code Engee Function:

  • BLOCK_NAME - the name of the block. Each block added to the Engee canvas has a name that can be referenced via this constant. For example, you can refer to BLOCK_NAME during error initialisation to print the block name in it.

  • START_TIME - the start of the simulation from the model settings.

  • END_TIME - end of the simulation from the model settings.

  • INPUT_SIGNAL_ATTRIBUTES - lists of attributes for each input port. For example, to find out the attributes of the first input signal - use INPUT_SIGNAL_ATTRIBUTES[1], where 1 is the first input port of the block Engee Function.

  • OUTPUT_SIGNAL_ATTRIBUTES - lists from attributes for each output port. For example, to find out the attributes of the first output - use OUTPUT_SIGNAL_ATTRIBUTES[1], where 1 is the first output port of the block Engee Function.

To find out additional information about a particular block port, you can refer to its signal attributes by adding the dot . after the constants INPUT_SIGNAL_ATTRIBUTES[i], where [i] is the input port number, and OUTPUT_SIGNAL_ATTRIBUTES[i], where [i] is the output port number, respectively. You can find out more information through the following call functions:

  • dimensions - the dimensionality of the signal. It can be abbreviated to dims.

  • type - signal type. Can be abbreviated to tp.

  • sample_time - calculation step. It represents a structure similar to the attributes of signals, which can be accessed through the point .. Two access functions are available:

    • period - period of the calculation step. The full reference function is sample_time.period. It can be shortened to st.p.

    • offset - offset of the calculation step. The full call function is sample_time.offset. Can be shortened to st.o.

  • direct_feedthrough - specifies whether the loop port opens. Used for input ports only (checks attributes for the input port only). Can be abbreviated to df.

Example model Engee Function with all constants and access functions:

engee function constants

struct Block <: AbstractCausalComponent end

function (c::Block)(t::Real, x1, x2)
    y1 = [START_TIME, STOP_TIME]
    y2 = collect(INPUT_SIGNAL_ATTRIBUTES[1].dimensions)
    y3 = OUTPUT_SIGNAL_ATTRIBUTES[1].dims[1]
    y4 = (INPUT_SIGNAL_ATTRIBUTES[2].type == Int64)
    y5 = (OUTPUT_SIGNAL_ATTRIBUTES[4].tp == Bool)
    y6 = INPUT_SIGNAL_ATTRIBUTES[1].sample_time.period
    y7 = OUTPUT_SIGNAL_ATTRIBUTES[1].st.p
    y8 = INPUT_SIGNAL_ATTRIBUTES[1].sample_time.offset
    y9 = OUTPUT_SIGNAL_ATTRIBUTES[2].st.o
    y10 = INPUT_SIGNAL_ATTRIBUTES[1].direct_feedthrough
    y11 = INPUT_SIGNAL_ATTRIBUTES[2].df
    return (y1, y2, y3, y4, y5, y6, y7, y8, y9, y10, y11)
end

Ports

Input

Input Port - input port
scalar | vector | matrix

Details

An input port specified as a scalar, vector or matrix.

Configure the input port in the Ports tab of the block using the following options:

  • Label - set the name of the input port. By default, the Label cell is not filled in (no name is set).

  • Type - input signal data type. Select one of the options:

    • Definite Type (all except Inherit') - checks that a specific type of signal is applied to the input port. Select a specific data type for the input port. Supported signals: `Float16, Float32, Float64, ComplexF32, ComplexF64, Bool, Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128.

    • Inherit (by default) - inherits the data type from the linked block. Can be any data type.

  • Size - dimension of the input signal:

    • Inherit all dimensions (-1 by default) - inherits the dimension of the signal applied to the input port (the signal can be of any dimension).

    • Defined dimensions - the input signal must have a specified number of elements. The dimensions are specified in Julia-notation (as a tuple), for example, (2,) for a one-dimensional signal of two elements or (2, 3, 4) for a multidimensional one. If -1 is specified, the dimensionality is inherited.

    • Inherit one of the dimensions - inherits the dimensionality of the signal applied to the input port with an explicit indication of the data structure. For example, (-1, 2) - it is expected that the first dimension is inherited and the second dimension is specified explicitly.

  • Output bus type - input bus type, replaces the input signal dimension Size in case the input signal data type Type has the BusSignal (bus) type selected. By default it has the value BusSignal{(), Tuple{}, ()}. In order for the block Engee Function understand which bus is coming to the input, it is sufficient to set Type to Inherit. Explicit specification of the type is only necessary for the output signal type.

    The block Engee Function does not inherit buses on output ports, although it can receive them on inputs. To inherit buses to output ports (for transmission to other blocks), the bus type must be explicitly specified in the Output bus type parameters.
  • Direct feedthrough - defines direct feedthrough connection:

    • If checked (by default), a direct feedthrough connection is available. This means that the output signal is controlled directly by the value of the input port.

    • If the checkbox is unchecked, a direct pass-through connection is not available. This means that the output signal will not be controlled by the input port value and allows the unit to open loops.

Output

Output port - output port
scalar | vector | matrix

Details

An output port specified as a scalar, vector or matrix.

Configure the output port in the Ports tab of the block using the following options:

  • Label - set the name of the output port. By default, the Label cell is not filled in (no name is set).

  • Type - the data type of the output signal type. Select one of the options:

    • Defined Type (all except Inherit) - define the output signal data type. Select a specific data type for the output signal type. Supported signals: Float16, Float32, Float64, ComplexF32, ComplexF64, Bool, Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128.

    • Inherit (by default) - inherits the output signal data type. Calculates the smallest common type when there are several input signals of different types. Can be any data type.

  • Size - dimension of the output signal:

    • Inherit all dimensions (-1 by default) - inherits the dimensionality of the signal applied to the output port. The output signal will have dimensions derived from the broadcast mechanism - Julia will automatically expand the dimensionality of a smaller data set to the dimensionality of a larger one to inherit the dimensionality correctly.

    • Defined dimensions - the output must have a specified number of elements. The dimensions are specified in Julia notation (as a tuple), e.g. (2,) for a one-dimensional signal of two elements or (2, 3, 4) for a multidimensional one. If -1 is specified, the dimensionality is inherited.

    • Inherit one of the dimensions - inherits the dimensionality of the signal applied to the output port with an explicit indication of the data structure. For example, (-1, 2) - it is expected that the first dimension is inherited and the second dimension is specified explicitly.

  • Output bus type - output bus type, replaces the input signal type Size if the output signal data type Type has the BusSignal (bus) type selected. By default it has the value BusSignal{(), Tuple{}, ()}. In order for a block to Engee Function to output a bus, it is necessary to explicitly specify its type.

Parameters

Main

Number of input ports - defines the number of input ports
1 (By default)

Details

Defines the number of input ports of the block. The value of the Number of input ports parameters will correspond to the number of input ports.

Number of output ports - determines the number of output ports
1 (By default)

Details

Defines the number of output ports of the block. The value of the Number of output ports parameters will correspond to the number of output ports.

Sample time - interval between calculation steps
`-1 (By default).

Details

Specify the interval between calculation steps as a non-negative number. To inherit a calculation step, set this parameter to -1.

Advanced

Use external cache for non-scalar output — use external cache for non-scalar outputs
off (by default) | on

Details

Specify usage of external cache for non-scalar (multidimensional) output signal to save RAM Engee. Used if the block has Engee Function has only one output port:

  • If unchecked (By default) - external cache is not used.

  • If the checkbox is checked - the output will be able to accept an additional cache argument, which must be taken into account in the source EngeeFunctionCode. If the block has one output port, then in the cell Step method code cache argument is automatically added to the list of function arguments, except when the port dimension is explicitly set as () (scalar). In the source code, it is required to write functors depending on the output dimension. Functors can be defined in the cell Common code:

    • If the output signal is scalar:

      function (c::Block)(t::Real, x)
          return c.g * x
      end
    • If the output signal is non-scalar:

      function (c::Block)(t::Real, cache, x)
          cache .= c.g .* x
          nothing
      end

      where t is the time, x is the argument (information from the input ports). The time parameter must be specified even if it is not present in the block parameters.

Sample time inheritance method — definition of the calculation step inheritance method
Default | Discrete | Continuous

Details
The Sample time inheritance method setting disappears from the Advanced tab if the Override sample time inheritance method checkbox is selected in the EngeeFunctionCode source code. In this case the calculation step inheritance method is determined by the Sample times inheritance method code of the function cell.

Defines the inheritance method of the calculation step depending on the selected value:

If -1 is specified in the Sample Time field, the calculation step is inherited according to the method specified in the Sample time inheritance method field depending on the selected value (Default, Discrete, Continuous). In other cases (Sample Time is not equal to -1 and SampleTime is greater than or equal to 0) - the block Engee Function works with the specified value of Sample Time field, ignoring Sample time inheritance method.
  • Default - inheritance method of the calculation step by default. The Default inheritance method is always used in the case when the block Engee Function is not discrete or continuous. The method gets any kind of calculation step. When this method is selected, the block Engee Function will inherit the calculation step according to the following principles:

    • If the block has no input ports - the output is a continuous calculation step.

    • If the input has all the same calculation steps - the output has the same calculation step as the input.

    • If there are continuous calculation steps among the input calculation steps - the output also has continuous calculation steps.

    • If there is a fixed-in-minor (FiM, Fixed-in-Minor) among the input calculation steps, there is no continuous calculation step and solver with variable step - the output is a fixed-in-minor.

    • If there are no continuous and fixed-in-small calculation steps at the input and not all calculation steps are equal - only discrete calculation steps at the input are considered, for which one of the variants is true:

      • If the greatest common divisor of the discrete computation steps is the same as one of the input computation steps or a constant-pitch solver is used - the output is the discrete computation step with the step of the greatest common divisor.

      • If the variable step solver and the greatest common divisor of the input discrete calculation steps do not coincide with any of the input calculation steps - the output is a fixed small.

  • Discrete - inheritance method to get discrete calculation step. If this method is selected, the block Engee Function will inherit the calculation step according to the following principles:

    • If the input has continuous or fixed small calculation steps - the output is a discrete calculation step with the solver step (even if the solver is a variable step).

    • If there are discrete calculation steps among the input - the output is a discrete calculation step with the largest common divisor from the input discrete calculation steps.

  • Continuous - inheritance method for obtaining a continuous calculation step regardless of the input calculation steps.

Example of propagate_sample_times function overriding with the work similar to the Default inheritance method:

+

function propagate_sample_times(inputs_sample_times::Vector{SampleTime}, fixed_solver::Bool)::SampleTime
    nonnegative_sample_times = filter(
        st -> st.period >= 0,
        collect(values(inputs_sample_times)),
    )
    finite_periods = filter(
        st -> !isinf(st.period),
        nonnegative_sample_times,
    ) .|> (st -> st.period)
    output_sample_time = if !isempty(nonnegative_sample_times)
        if allequal(nonnegative_sample_times)
            first(nonnegative_sample_times)
        elseif any(st -> st.period == 0 // 1 && st.mode == :Continuous, nonnegative_sample_times)
            (period = 0 // 1, offset = 0 // 1, mode = :Continuous)
        elseif any(st -> st.mode == :FiM, nonnegative_sample_times) && !fixed_solver
            (period = 0 // 1, offset = 0 // 1, mode = :FiM)
        elseif (
            all(x -> x.period > 0 // 1, nonnegative_sample_times) &&
            (fixed_solver || gcd(finite_periods) in finite_periods)
            )
            (period = gcd(finite_periods), offset = 0 // 1, mode = :Discrete)
        else
            (period = 0 // 1, offset = 0 // 1, mode = :FiM)
        end
    else
        (period = 0 // 1, offset = 0 // 1, mode = :Continuous)
    end
    return output_sample_time
end
If the propagate_sample_times function returns (period = 0 // 1, offset = 0 // 1, mode = :Discrete), then such a calculation step will be taken as discrete with the solver step.

Setting parameters

Number of parameters - specify the number of parameters
1 (By default)

Details

Number of parameters used in the block.

Parameter - defines a parameter as a variable
scalar | vector | array

Details

Defines the parameters for usage in the source code of the block Engee Function. Parameters can be specified in the parameter:

  • Name - name of the parameters;

  • Value - value of the parameters. As values you can set any expressions in Julia language.

The value and name of the parameters can be changed in the Parameters tab. The name of the first parameter (present by default) is gain, whose value is 2. The new parameters are named parameter2 and then in ascending order. The value of the new parameters is 0 by default.

Global variables available in the Engee variables window variables icon can be set as Parameters tab variables to be inserted into the source code of the block Engee Function. If the name of the parameters and the global variable are the same, the value of the parameter will be automatically substituted from the global variable.

To set a bus parameter, set the Value parameter to the bus value as a named tuple, e.g. (s1 = 5, s2 = 4).


It is important to distinguish between block parameters, which are set in the Parameters tab and are global variables in the source code Engee Function, from the Engee global variables in the Variables window variables icon. Although you can use the values and names of Engee global variables in block parameters, these entities are completely different. You cannot control Engee global variables through a block’s parameters or from its source code. However, for source code, block parameters (from the Parameters tab) are global variables and can be used in any part of the block without being bound to a specific function or code block.

Let’s consider the case when global variables fully correspond to the variables in the source code. For example, we have defined three global variables , , with the values 1, 2, 3 respectively. All three global variables are used as parameters of the block Engee Function:

engee function param explain 1

Then the source code with parameters added would look like this:

struct Block <: AbstractCausalComponent
    a::Real
    b::Real
    c::Real

    function Block()
        new(a_param, b_param, c_param)
    end
end

function (c::Block)(t::Real, x::Vector{<:Real})
    return (c.a .* x .+ c.b) ./ c.c
end

This source code defines the Block structure, which allows you to customise the behaviour of the component. The example uses the block parameter names a_param, b_param, and c_param to define the parameters of the structure , , and respectively. The code also defines a method function(c::Block)(t::Real, x::Vector{<:Real}) that scales each element of the vector x by the block parameter , adds , and divides the result by . This allows flexibility to modify and normalise the vector x according to the values of the block parameters.


Consider the case where only the Parameters tab parameters are used:

struct Block <: AbstractCausalComponent end

function (c::Block)(t::Real, x::Vector{<:Real})
    return (a_param .* x .+ b_param) ./ c_param
end

These parameters will be global variables in the block code. This means that they will always be available in any part of the block code without having to define them repeatedly in each function or code block. This greatly simplifies the code and makes it easy to change parameters in the Parameters tab without affecting the source code.


Consider the case where the parameters are not completely consistent with the source code. For example, there is a parameter a_param equal to 100. In the source code there is a field of the structure :

struct Block <: AbstractCausalComponent
    a::Real

    function Block()
        new(a_param/10)
    end
end

function (c::Block)(t::Real, x::Vector{<:Real})
    c.a
end

In this code, the a_param parameter is used to initialise the field of the struct Block structure through its constructor, which divides the parameter value by 10. In this case, the block functor returns the field .


Variables can be made global right in the source code:

a = 1;
b = 2;
с = 3;

struct Block <: AbstractCausalComponent; end

function (c::Block)(t::Real, x::Vector{<:Real})
    return (a .* x .+ b) ./ c
end

The code creates a Block structure and defines a function that performs mathematical operations on the global variables , and , applying them to the x variable.


If you don’t want to use parameters from the Parameters tab, you can initialise the variables directly in the source code:

struct Block <: AbstractCausalComponent; end

function (c::Block)(t::Real, x)
    a = 1;
    b = 2;
    c = 3;
    return (a .* x .+ b) ./ c
end

The code creates a Block structure and defines a function that performs mathematical operations on the local variables , and , applying them to the x variable.

Let’s consider the most efficient and correct way of execution. To make the block Engee Function work correctly, set the Use external cache for non-scalar output checkbox on the Main tab of the block. Engee Function. The source code will look like this:

struct Block{Ta, Tb, Tc} <: AbstractCausalComponent
    a::Ta
    b::Tb
    c::Tc

    function Block()
        Ta = typeof(a_param); Tb = typeof(b_param); Tc = typeof(c_param)
        all(isreal, (a_param, b_param, c_param)) ||
          error("Параметры блока должны быть вещественными")
        all(x->isempty(size(x)), (a_param, b_param, c_param)) ||
          error("Параметры блока должны быть скалярами")
        new{Ta, Tb, Tc}(a_param, b_param, c_param)
    end
end

function (c::Block)(t::Real, cache::Vector{<:Real}, x::Vector{<:Real})
    cache .= (c.a .* x .+ c.b) ./ c.c
    nothing
end
This code can be written only in a cell *Common code*because standard cells do not allow editing the component structure definition and the functor signature.

The fields , and of the structure represent the parameters of the block, which are strictly type-checked in the constructor. Each of these parameters must be a real scalar (Real) to ensure accurate calculations at runtime.

The Block() constructor checks the types of the passed parameters , and . If at least one of them is not a real scalar or has mismatched dimensions (must be scalars), the constructor generates an error with an appropriate message. After checking, the constructor initialises the structure fields with the values , and of the specified types Ta, Tb and Tc.

A computable function defined for Block instances takes the time t, an external cache and a vector x of real numbers. This function uses the fields , and of the Block structure to compute values which are then written to the cache. This avoids unnecessary memory allocations by reusing the provided cache.

Thus, the Block structure provides strict data type management and efficient usage of resources through the usage of an external cache to store the results of calculations.


If you want to change the parameters of a block, you must use a mutable structure. Let’s look at an example:

mutable struct Counter{T} <: AbstractCausalComponent
    limit::T
    iter::T

    function Counter()
      isempty(size(limit)) || error("Предел блока $BLOCK_NAME должен быть скаляром")
      isreal(limit) || error("Предел блока $BLOCK_NAME должен быть вещественным числом")
      T = typeof(limit)
      iter = zero(T)
      new{T}(limit, iter)
    end
end

function (c::Counter)(t::Real)
    return c.iter
end

function update!(c::Counter, t::Real)
  c.iter += 1
  if c.iter > c.limit
    c.iter = zero(c.iter)
  end
  c
end

The Counter structure is a mutable data type that is used to count iterations with a given limit and is strictly typed. The limit and iter fields of the structure represent parameters of the block:

  • limit is the limit value of the counter;

  • iter is the current value of the counter.

In the constructor of the structure, the limit parameters are checked (validated) whether the parameter is a scalar and real data type. After that, the iter field is initialised with a null value of the corresponding T type. The update! function updates the state of the counter , increasing the value of iter by one each time it is called. If the current value of iter exceeds limit, it is reset to zero and allows the counter to cycle back to the initial state.

In the source code of a block Engee Function it is possible to use include referring to external code. This allows variables from external code (if any) to be initialised in the source code.
The actual data type, as well as the support for possible data types, depends on the user code within the block.

Example code

This example presents a simplified implementation of the block Discrete-Time Integrator, based on the integration of Julia code into the Engee model. The direct Euler method is chosen as the integration method. On the Advanced tab of the block Engee Function set the Discrete value of the Sample time inheritance method parameters. Then fill in the cells of the source code as follows:

  • In the cell Common code:

    mutable struct Block{T} <: AbstractCausalComponent
        const dt::Float64
        state::T
        gain::Float64
    
        function Block()
            dt = OUTPUT_SIGNAL_ATTRIBUTES[1].sample_time.period
            state = initial_condition
            gain = k
            new{typeof(state)}(dt, state, gain)
        end
    end
  • In the cell. Step method code:

        return c.state
  • In the Update method code cell:

        c.state += in1 * c.dt * c.gain
        return c

The following source code will result:

engee function example 1

The parameters initial_condition and k are initialised in the Parameters tab of the block settings Engee Function:

engee function example 2

In the first step of the model simulation, the internal state of the c.state block is initialised by the value of the initial_condition parameters.

Then at each step of the calculation the block returns the internal state c.state as an output and recalculates its value in the update! method.

The structure of the component is redefined in the cell Common code*instead of in *Component struct code, because a more flexible definition is required: the structure must be mutable and parameterised with a T type corresponding to the state type. The standard definition in Component struct code is only suitable for immutable and non-parameterised structures.

Annotations

Annotations in Engee Function allow to display parameters of a block directly under its name in the model. To add them, open settings window debug article icon 1 block Engee Function and go to the Annotation tab. Select the desired block property markers and add them in the text editor.

engee function annotations

On this tab:

  • On the left is a list of available parameters (except hidden parameters).

  • On the right is a text editor, where you can specify an annotation with markers in the format %<ИмяПараметра>.

  • The parameters can be transferred manually, via autocomplete or with the button engee function annotations 1.

After exiting the editor (e.g. by clicking outside it), the annotation is applied: the markers are automatically replaced by the actual values of the parameters, and the resulting text is displayed below the block name (or above it, if the name is placed on top).

To delete annotations, the corresponding marker in the editor must be deleted.

Available markers

The property marker is automatically replaced by the current value of the parameters. The following markers are available:

  • *Ports: %<Inputs>, %<Outputs> - number of input and output ports; %<InputPort1Type>, %<OutputPort1Type>, %<InputPort1Size>, %<OutputPort1Size> - data type and dimensionality of signals.

  • *Temporal characteristics: %<SampleTime> - discretisation; %<SampleTimeInheritanceMethod> - discretisation inheritance method.

  • *Code blocks: %<ComponentStructCode>, %<StepMethodCode> - step structure and method code.

  • Parameters: %<Parameters>, %<Parameter1Name>, %<Parameter1Value> - names and values of parameters.

  • Inclusion flags: %<DefineComponentStruct>, %<UseCommonCode>, %<DefineStepMethod>, %<DefineUpdateMethod>, %<DefineTerminateMethod> - inclusion of corresponding code sections.

  • *Reference methods: %<OverrideTypesInhMethod>, %<OverrideDimsInhMethod>, %<OverrideSampleTimeInhMethod> - type inheritance settings.

  • Other: %<UseExternalCache> - usage of external cache.