Engee documentation

Engee Function

Julia code usage in models.

blockType: EngeeFunction

Path in the library:

/Basic/User-Defined Functions/Engee Function

Description

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

For more information about the Julia programming language, see Programming.
In the Engee Function block it is allowed to use most of the features of 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 to the model the block 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 and Step method code (cells can be hidden):

engee function all start cell

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

include("/user/engeefunction/source.jl")
The entire code of the Engee Function block can be written in the Common code cell, giving you 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 functionality of the block Engee Function. Let’s consider them in detail:

  • Information cell (non-editable) - automatically displays variables of the block Engee Function, attributes of input and output signals type (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 methods 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, in which the structure of the block component Engee Function (inherited from type AbstractCausalComponent) is defined. 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 the 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 (due to the uneditable struct Block <: AbstractCausalComponent), you can disable Define component struct to remove the cell, and define the component structure manually in Common code. The same goes for any functions from the method management menu of engee function all methods - you can write your own code in place of the standard cells in Common code.

    engee function common code

    Declaring component and functor is mandatory for Engee Function to work. If Define component struct and Define step method are disabled, their code must be set in Common code, otherwise the block will not work.
    To override inheritance functions (Override, see below), you must first enable the corresponding cell, erase its contents, and then write new code in Common code.
  • Define step method - adds the cell Step method code, which defines the Step method that calculates the output signals of the block Engee Function. The method signature is generated automatically depending on the values of the corresponding labels (Label) 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 lines 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 the Update method code cell, where the update! method updates the internal state of the block Engee Function at each simulation step. 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 need to define several update! methods or specify a method with a different signature, the Update method code cell can be disabled and you can write the code in the Common code cell. 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 simulation of the block Engee Function is terminated (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 Types inheritance method cell that overrides the type inheritance method.

    For more information about the 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 block executable code, use the following constants inside 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 the initialisation of an error to print the name of the block in it.

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

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

  • INPUT_SIGNAL_ATTRIBUTES - lists from 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 learn additional information about a particular block port, you can refer to its signal attributes by adding the dot . after the constants INPUT_SIGNAL_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 - the type of the signal. 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 abbreviated to st.p.

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

  • direct_feedthrough - indicates 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 address 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

Working with fixed point and custom buses

The block Engee Function, as well as command line img 41 1 2 Engee, support fixed-point (Fixed) and custom bus types (BusSignal). These constructs help control the behaviour of operations when dealing with integer, real, fixed and complex data types.

Fixed-Point types and functions (Fixed-Point)

The article Fixed-Point Arithmetic (Fixed-Point) in Engee describes fixed-point operations in Engee. The constructions given in the article are also valid for the block Engee Function, so the block supports:

  • FixedPoint - abstract type of all fixed numbers;

  • Fixed - concrete type of fixed number, created manually with bit representation;

  • fi(…​) - creation of a value of type Fixed using a numeric value and parameters of the representation;

  • fixdt(…​) - creation of Fixed type with specifying sign, width and number of fractional bits.

For example:

a = fi(5, 1, 16, 4)     # Знаковое число, 16 бит, 4 дробных
b = fi(10, 0, 8, 0)     # Беззнаковое целое 8 бит
T = fixdt(1, 24, 8)     # Тип Fixed с 24 битами, 8 из них — дробные
c = Fixed(0x01ff, T)    # Создание фиксированного числа напрямую по битовому представлению

Fixed numbers in Engee Function can:

Working with custom busbar types (BusSignal)

Custom Bus Types allows you to specify input and output signal types as structured tuples (NamedTuple) with descriptions of names, types and dimensions inside Engee Function. Available functions:

  • BusSignal{...} - bus type with names, types and dimensions;

  • get_bus_names(type) - get list of signal names;

  • get_bus_types(type) - get signal types;

  • get_bus_dimensions(type) - get signal dimensions;

  • get_names_types_dims(type) - get all at once;

  • get_bus_signal_type(::NamedTuple) - define BusSignal type by value.

Example of bus type definition and analysis:

bus = (a = 1, b = [2.0, 3.0], c = (x = 3, y = 4))
bus_type = get_bus_signal_type(bus)

get_bus_names(bus_type)       # => (:a, :b, :c)
get_bus_types(bus_type)       # => (Int64, Vector{Float64}, NamedTuple{...})
get_bus_dimensions(bus_type)  # => ((), (2,), ((), ()))

It is possible to describe tyres explicitly:

MyBus = BusSignal{(:s1, :s2), Tuple{Int64, Float64}, ((), ())}
signal = MyBus((s1 = 5, s2 = 6.4))

Nested buses are also supported:

Inner = BusSignal{(:x, :y), Tuple{Int, Int}, ((), ())}
Outer = BusSignal{(:a, :b), Tuple{Float64, Inner}, ((), ())}

Usage in the block Engee Function

The Fixed and BusSignal types can be used in different parts of the block Engee Function:

  • In the block parameters - fixed point values can be set via fi(…​) and bus structures can be passed as NamedTuple. The bus type can be defined automatically via get_bus_signal_type(…​).

  • In the Common code cell - you can define data types (Fixed, BusSignal{…​}), component structures (struct Block), create auxiliary functions or initialise values. You can also move basic logic here if Step method code or Component struct code is disabled.

  • In the Component struct code cell - values of Fixed type can be used when describing fields of the structure, as well as BusSignal types if the fields represent composite signals.

  • In the Step method code cell - the basic logic of the block is implemented. Here fixed numbers and bus signals can participate in calculations, comparisons and structure processing.

  • In the Types inheritance method cell - you can use Fixed and BusSignal types to analyse the inputs and set the output type of the component.

  • In the Dimensions inheritance method cell - you can use Fixed data and values from buses to define the output dimensions.

  • In the Update method code cell - if the block has an internal state, fields like Fixed or BusSignal can be used to store or change that state at each simulation step.

  • In the Terminate method code cell - usage of these types to record the final state is allowed if the structure contains the appropriate fields.

  • In the Common types and dimensions inheritance method cell - if both types and dimensions are to be handled simultaneously, Fixed and BusSignal can also participate in the calculation logic.

Consequently, Fixed, fi(…​), fixdt(…​), BusSignal and get_bus functions are applicable in all aspects of the configuration and operation of the Engee Function block - both at runtime and at signal type and dimension generation. They can be freely used both in parameters and in source code in all cells (if their operation is correctly observed).

Conversion functions and typed arithmetic operations: econvert, esum, emul, ediv.

Functions are available in the block Engee Function, as well as in command line img 41 1 2 Engee that allow you to perform arithmetic operations specifying the output type, to convert values between types with rounding and overflow control, and to predefine the type of the result using specialised rules (_promote_type). These functions are used in the logic Engee Function, written in source code cells Step method code, Common code and others.

Available functions:

  • econvert - converting values between types;

  • esum, esub, emul, ediv - arithmetic operations with specifying the output type;

  • emul! - matrix multiplication with result caching;

  • *_promote_type - functions of output result by input types.

These functions can be applied to both scalars and arrays. The result is always returned in the type explicitly specified by the user (or output automatically), which makes the block behaviour stable and expected.

Supported types

All functions in this suite work with the following types:

  • Real: Float16, Float32, Float64.

  • Integer: Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128.

  • Logical: Bool

  • Fixed: Fixed (created via fi(…​) or fixdt(…​))

  • Complex: Complex{T}, where T is any of the above types

Functions can also be used with arrays and vectorised expressions, using the dot syntax (.). Let’s consider the functions in more detail below.

Converting numbers using econvert

The econvert function allows you to convert a value to a given type by specifying the rounding method and the way of overflow handling:

econvert(type::Type{T1}, var::T2; rounding_mode::RoundingMode=RoundDown, overflow::Bool=true)

Here:

  • type is the type into which the value is to be converted. If var is a complex number, the base real type is specified. For example, econvert(Float32, Complex{Int16}(1 + 2im)) will return Complex{Float32};

  • var - the value to convert;

  • rounding_mode - rounding method (RoundDown, RoundUp, RoundNearest, RoundNearestTiesAway, RoundToZero);

  • overflow - if true, wrap behaviour is used; if false, saturation is enabled - the value is limited by the type range.

The econvert function can be used for type conversion before comparisons, inside arithmetic expressions, when processing signals in conditions and other scenarios.

Type-controlled arithmetic with esum, esub, emul, ediv, `emul!

The functions of the typed arithmetic operations esum, esub, emul, ediv, emul! have the same interface:

esum(x1, x2, out_type, overflow=true, rm=RoundDown)
esub(x1, x2, out_type, overflow=true, rm=RoundDown)
emul(x1, x2, out_type, overflow=true, rm=RoundDown)
ediv(x1, x2, out_type, overflow=true, rm=RoundDown)
emul!(cache, x1, x2, out_type, overflow=true, rm=RoundDown)

Arguments:

  • x1, x2 - arguments of the operation (can be scalars or arrays);

  • out_type - the type in which the result is produced and returned;

  • overflow - enable overflow (true) or saturation (false);

  • rm - rounding method (see the list above).

The functions esum, esub, emul, ediv return the value corresponding to the specified out_type.

The emul! function is used for speeding up when working with matrices: it does not create a new array, but writes the result into the already allocated cache. It is important that eltype(cache) coincides with out_type.

The result depends on the out_type: it affects not only the final result, but also the behaviour of intermediate computations - which is especially important for Fixed and Complex.

Outputting the result type with *_promote_type

If the block parameters (e.g. SumType or MulType) are specified as an Inherit string, the result type is determined automatically based on the input data types using the *_promote_type functions.

If you need to calculate the result type of an operation, use the functions listed above:

  • Determines the output type when adding n values of the same type T:

    sum_promote_type(::Type{T}, n::Integer=1, hdlsetup::Bool=false)
  • Determines the output type when adding two different types T1 and T2 by n values:

    sum_promote_type(::Type{T1}, ::Type{T2}, n::Integer=1, hdlsetup::Bool=false)
  • Defines the output type when adding an arbitrary number of arguments with different types:

    sum_promote_type_from_inputs(types::Type...; hdlsetup::Bool=false)
  • Determines the accumulator type when adding n values of the same type T:

    sum_accumulator_promote_type(::Type{T}, n::Integer=1, hdlsetup::Bool=false)
  • Determines the type of accumulator when adding two different types T1 and T2 by n values:

    sum_accumulator_promote_type(::Type{T1}, ::Type{T2}, n::Integer=1, hdlsetup::Bool=false)
  • Determines the accumulator type when adding an arbitrary number of arguments with different types:

    sum_accumulator_promote_type_from_inputs(types::Type...; hdlsetup::Bool=false)
  • Determines the output type when multiplying values of the same type T:

    mul_promote_type(::Type{T}, hdlsetup::Bool=false)
  • Defines the output type when multiplying two different types T1 and T2:

    mul_promote_type(::Type{T1}, ::Type{T2}, hdlsetup::Bool=false)
  • Determines the output type when dividing a single value of type T:

    div_promote_type(::Type{T}, hdlsetup::Bool=false)
  • Determines the output type when dividing a value of type T1 by a value of type T2:

    div_promote_type(::Type{T1}, ::Type{T2}, hdlsetup::Bool=false)

    Here:

    • hdlsetup is a logical parameter that determines whether to consider the specific hardware platform (TargetHardware). By default:

      hdlsetup = TargetHardware == "C" ? false : true

Functions are conveniently used in the Types inheritance method cell to accurately determine the output type of a block based on inputs and parameters.

Example usage

Below is an example of a component implementing the expression a*x + b. The types of intermediate multiplication and final addition are specified through the parameters MulType and SumType. If Inherit is specified, the type is derived automatically.

Example usage of `a*x + b`.

engee function example 3

Parameters Engee Function:

a = fi(5, 1, 16, 4)
b = fi(11, 1, 24, 7)
MulType = "Inherit"
SumType = "Inherit"

Block code (in cell Common code, Dimensions inheritance method and Types inheritance method):

  • Common code cell:

    struct Block{SumType, MulType, aType, bType} <: AbstractCausalComponent
        a::aType
        b::bType
        function Block()
            sum_type = OUTPUT_SIGNAL_ATTRIBUTES[1].type
            mul_type = if MulType == "Inherit"
                mul_promote_type(INPUT_SIGNAL_ATTRIBUTES[1].type, eltype(a))
            else
                MulType
            end
            new{sum_type, mul_type, typeof(a), typeof(b)}(a, b)
        end
    end
  • Cell Dimensions inheritance method:

    function propagate_dimensions(inputs_dimensions::Vector{<:Dimensions})::Vector{<:Dimensions}
        input_dimensions = first(inputs_dimensions)
        mock_input = zeros(input_dimensions)
        mock_output = mock_input .* a .* b
        return [size(mock_output)]
    end
  • Cell Types inheritance method:

    function propagate_types(inputs_types::Vector{DataType})::Vector{DataType}
        mul_type = if MulType == "Inherit"
            mul_promote_type(inputs_types[1], eltype(a))
        else
            MulType
        end
        sum_type = if SumType == "Inherit"
            sum_promote_type(mul_type, eltype(b))
        else
            SumType
        end
        return [sum_type]
    end

Thus, the econvert, esum, emul, ediv functions and promote_type rules allow you to control the types and behaviour of calculations within the block Engee Function. They support all major numeric types, including fixed point and complex values, and can be used in calculations, comparisons, inheritance logic, and customisation of component behaviour using the Engee Function block .

Diagnostic functions warning, stop_simulation, pause_simulation, info

The warning, stop_simulation, pause_simulation, and info functions are also available in the source code of the Engee Function block to interact with the model diagnostic system during the simulation phase, allowing messages to be displayed in diagnostic window model diagnosis main. These functions can be used within the following source code cells:

  • *Component struct code.

  • Step method code

  • Update method code

  • Terminate method code

  • Common code

The warning, stop_simulation, pause_simulation, info functions can only be used in code sections that are executed during model simulation.

Thus, although these functions are formally allowed in the Component struct code cell, in practice it makes no sense to place them there: this cell is intended only for describing the block structure and not for executable logic. To make diagnostic functions work correctly, they should be placed inside the supported simulation methods: Step method, Update method, Terminate method, or in Common code if the corresponding method is overridden there. Example of correct placement:

  • Component struct code:

    #
    struct Block <: AbstractCausalComponent
        g::Real
        function Block()
            new(gain)
        end
    end
  • Common code, the functor function (c::Block)(t::Real, in1) is taken from Step method code, the method itself is disabled:

    function (c::Block)(t::Real, in1)
        if t == 5.0
            warning("time == $t")
        end
        return c.g .* in1
    end

By analogy, you can override any supported method called during the simulation (e.g. update!, terminate, step) in the Common code cell manually - if you disable the corresponding standard cell (e.g. Define update method, Define step method, etc.).

The warning, stop_simulation, pause_simulation and info functions may be used in the Common code cell for correct operation:

  • Within functions that override the behaviour of methods responsible for executing the model - such as step, update!, terminate!. For example, if a functor normally defined in the Step method code cell is defined in Common code, then the diagnostic functions within it will work correctly.

  • Inside auxiliary functions that are called from methods responsible for executing the model. That is, if an auxiliary function is defined in Common code and is used, for example, in step, then calls of diagnostic functions inside it will also work correctly.

    Overriding update! or other methods does not replace the mandatory implementation of step (function (c::Block)(t, in…​)). Engee requires this function as an entry point into the simulation.

An example of a correct override of update! in Common code:

# Структура с Component struct code (если отключена, то должна быть обязательно вынесена в Common code)
struct Block <: AbstractCausalComponent
    g::Real
    function Block()
        new(gain)
    end
end

# Step method (обязательный метод, вызываемый на каждом шаге симуляции)
function (c::Block)(t::Real, in1)
    c = update!(c, t, in1)
    return c.g .* in1
end

# Переопределенный метод update!
function update!(c::Block, t::Real, in1)
    if t == 5.0
        info("update triggered at t = $t")  # диагностическое сообщение
    end
    return c
end

This will result in the following messages in the model diagnostics window:

engee function continue 1


Next, let’s take a closer look at the diagnostic functions themselves:

  • warning - the warning(msg::String) function outputs a warning message. The simulation continues while doing so. This can be useful for pointing out non-critical problems or states that require attention but do not stop execution. Example:

    if t == 5.0 || t == 7.5
        warning("time == $t")
    end
  • stop_simulation - The stop_simulation(msg::String) function immediately terminates the simulation and displays a termination message. It is used to indicate a critical condition where it is not possible or desirable to continue the simulation. Example:

    if t == 5.0
        stop_simulation("time == $t")
    end
  • pause_simulation - the pause_simulation(msg::String) function pauses simulation execution and displays the specified pause message. The simulation can be resumed manually using the button Continue:

    engee function continue

    This function can be useful to analyse the state of the model at a point in time of interest. Example:

    if t == 5.0
        pause_simulation("time == $t")
    end
  • info - the info(msg::String) function outputs an informational message. It is used to display intermediate values without affecting simulation execution. Example:

    if t == 5.0 || t == 7.5
        info("time == $t")
    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 have 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{}, ()}. To make the Engee Function block 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 Engee Function block does not inherit buses on output ports, although it can receive them on inputs. To inherit buses to output ports (for passing to other blocks), you must explicitly set the bus type 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 BusSignal (bus) is selected for the output signal type Type. By default, it has the value BusSignal{(), Tuple{}, ()}. In order for the Engee Function block to output a bus, you must explicitly specify the bus 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 when the Engee Function block has only one output port:

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

  • If the checkbox is checked - the output signal will be able to accept an additional cache argument, which must be considered in the source EngeeFunctionCode. If the block has one output port, the cache argument is automatically added to the list of function arguments in cell Step method code, except when the port dimension is explicitly set as () (scalar). In the source code, it is required to write functors depending on the dimensionality of the output signal. The 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) - 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 when the block Engee Function is not discrete or continuous. The method gets any kind of calculation step. When this method is selected, the Engee Function block will inherit the calculation step as follows:

    • 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 a parameter for usage in the source code of the block Engee Function. Parameters can be set 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 variable window variables icon can be set as Parameters tab variables for insertion 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 of Engee Function, and the Engee global variables in the variables window of 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 will 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 Engee Function block work correctly, check the Use external cache for non-scalar output option on the Main tab of the Engee Function block. 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
Such code can be written only in the cell Common code, because the standard cells do not allow editing the component structure definition and the functor signature.

The fields , and of the structure represent 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 the Engee Function block, you can use include to reference 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 custom 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 Engee Function block set the Discrete value for the Sample time inheritance method parameters. Then fill in the source code cells 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

At 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, not in Component struct code, because a more flexible definition is required: the structure must be mutable and parameterised by a T type corresponding to the state type. The standard definition in Component struct code is only suitable for immutable and non-parameterised structures.

Advanced usage

The Engee Function block allows you to specify the behaviour of a component using your own code, without having to build it from prebuilt blocks. This gives you the ability to manually control output signal types and dimensions, use caching to improve performance, disable direct passing of inputs to outputs, and set your own data refresh period.

For example, a page posted in Engee Community at link provides practical examples of advanced usage of the block Engee Function:

  • Transforming input data into a vector with overriding output parameters;

  • Usage of caching and strictly typed structures to improve performance;

  • Implementing Direct feedthrough-free blocks to break algebraic loops;

  • Custom output sampling period.

Annotations

Annotations in Engee Function allow you 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 click 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.