Engee documentation

Engee Function

Using Julia code in models.

blockType: EngeeFunction

Path in the library:

/Basic/User-Defined Functions/Engee Function

Description

Block Engee Function allows you to use Julia code in Engee models.

Read more about the Julia programming language on the page Programming.
In the block Engee Function It is acceptable to use most of the features of the Julia language. However, the use of the Pkg package manager in the block is not provided.

Using

To integrate the Julia code into the Engee model, you must:

  1. Add a block to the model Engee Function from the Basic section/Custom functions Block libraries 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 functional cells with Julia code. By default, three cells are available: auxiliary (non-editable), Component struct code and Step method code (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 (for the description of the cell, see below):

include("/user/engeefunction/source.jl")
The entire block code Engee Function you can write in the cell Common code by gaining 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 appropriate cells.:

engee function choose methodsengee function choose methods 1

Each cell is responsible for the unique functionality of the block Engee Function. Let’s look at them in more detail:

  • Information cell (non—editable) - automatically displays block variables Engee Function, attributes of input and output signals (dimension, type, discreteness) and other user-defined parameters. Its content is updated dynamically depending on the block settings. The cell is always active, but it is not selected in the method management menu. engee function all methods. It has a semi-transparent text that displays hints.

    engee function start cell

    Changing the block parameters affects not only the contents of the information cell, but also the hints in other cells, which are also displayed in translucent text.
  • Define component structure — adds a cell Component struct code, which defines the structure of the block component Engee Function (inherited from the AbstractCausalComponent type). The structure fields are defined between the non-editable strings struct Block <: AbstractCausalComponent and end'. By default, the `g parameter is created, initialized by the value of the gain block, and the Block() constructor does not accept 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 if it is not suitable (due to the non-editability of struct Block <: AbstractCausalComponent), then you can disable Define component struct to delete the cell and define the component structure manually in Common code. The same applies to any functions from the method management menu. engee function all methods — instead of standard cells, you can write your own code in Common code.

    engee function common code

    Declaring a component and a functor is required to work. Engee Function. If Define component structure and Define step method are disabled, then their code must be set in Common code otherwise, the block will not work.
    To redefine inheritance functions (Override, see below), you first need to include the corresponding cell, erase its contents, and then write new code in Common code.
  • Define step method — adds a 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 port labels in the Ports tab. The method is represented as a functor (for more information, see here) and is called at each step of the simulation. Fields are defined between the non-editable lines of function (c::Block)(t::Real, in1)`and `end'. The first argument is `t::Real is the simulation time, then the input signal variables are transmitted. The calculated values of the outputs are returned using the keyword return.

    engee function define step method

  • Define update method — adds a cell Update method code in which the method update! 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 transmitted. If the block has no internal state, the method can remain empty and simply return `c'.

    engee function update method code

    If you need to define multiple update!' methods or set a method with a different signature, then you can disable the Update method code cell and write the code in the cell Common code. The compiler will automatically detect the presence of the `update!' method.`and uses it for simulation.
  • Define terminate method code — adds the Terminate method code cell, which is executed at the end of the block simulation Engee Function (using the terminate! method). The first argument of `c::Block' is the block structure. By default, the method does not perform any additional actions and simply returns `c'.

    engee function terminate method code

  • Override type inheritance method — adds the Types inheritance method cell, which overrides the type inheritance method.

    _ Learn more about the type inheritance method_

    Sets the type inheritance method:

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

    • If the box is checked, the input/output port types are inherited according to the rules specified in the propagate_types function the Types inheritance method cells in the source code.

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

    Default cell code:

    function propagate_types(inputs_types::Vector{DataType})::Vector{DataType}
    # A function that returns an array of output signal types.
    # It is ignored if the default algorithm is used.
    # In this case, the type of input signal and the type of
    # the parameter of the `gain` block.
        input_type = first(inputs_types)
        # promote_type returns the type to which the argument types are given.
        # during arithmetic operations with objects of these types.
        output_type = promote_type(input_type, eltype(gain))
        return [output_type]
    end

    Here, the common element of the input signal and the element of the parameter specified in the block settings in the Setting up the parameters section are taken to inherit types.

  • Override dimensions inheritance method — adds a cell Dimensions inheritance method, which redefines the method of inheritance of dimensions.

    _ Learn more about the method of dimension inheritance_

    Sets the method of inheritance of dimensions:

    • If the checkbox is unchecked (by default), the dimensions of the input/output ports are inherited according to the rules specified in the description of the input/output ports.

    • If the box is checked, the dimensions of the input/output ports are inherited according to the rules specified in the function propagate_dimensions the Dimensions inheritance method cells in the source code.

      • The 'propagate_dimensions` function takes an array of tuples (dimensions) for each input signal and returns an array of dimensions for the output.

    Default cell code:

    function propagate_dimensions(inputs_dimensions::Vector{<:Dimensions})::Vector{<:Dimensions}
    # A function that returns an array of output signal dimensions.
    # It is ignored if the default algorithm is used.
    # In this case, the dimensions of the input signal are taken into account and
    # the parameter of the 'gain` block.
        input_dimensions = first(inputs_dimensions)
        mock_input = zeros(input_dimensions)
        mock_output = mock_input .* gain
        return [size(mock_output)]
    end

    Here, to inherit dimensions, an array with the necessary dimensions (mock_input) is taken, consisting of zeros, and multiplied by the element of the parameter specified in the block settings in the Setting up the parameters section, after which 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 redefine inheritance of types and dimensions simultaneously.

    _ Learn more about the method of inheritance of types and dimensions_

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

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

    • If the box is checked, the dimensions and types of input/output ports are inherited according to the rules specified in the function propagate_types_and_dimensions cells Common types and dimensions inheritance method in the source code.

    Default cell code:

    function propagate_types_and_dimensions(inputs_types::Vector{DataType}, inputs_dimensions::Vector{<:Dimensions})::Tuple{Vector{DataType}, Vector{<:Dimensions}}
    # A function that returns an array of signal types and an array
    # the dimensions of the output signals. This function can be used
    # if necessary, redefine the inheritance algorithm at the same time.
    # types of signals, and an algorithm for inheritance of dimensions.
        outputs_types = propagate_types(inputs_types)
        outputs_dimensions = propagate_dimensions(inputs_dimensions)
        return outputs_types, outputs_dimensions
    end

    Dependencies

    To use this cell, check the Override type inheritance method and Override dimensions inheritance method boxes.

  • Override sample time inheritance method — adds a cell Sample times inheritance method, which redefines the inheritance method of the calculation step.

    _ Learn more about the method of inheritance of the calculation step_
    The structure code of the calculation step SampleTime and the propagate_sample_times functions will not be automatically added to the EngeeFunctionCode source code of the old Engee models. To refine the old models, add the structure of the calculation step and the function yourself.

    Sets the inheritance method for the calculation step:

    • If the checkbox is unchecked (by default), the preset method of inheritance of the calculation step is used (by default, Default) from the Sample time inheritance method parameter of the Advanced tab. Read more about the pre-installed methods. below.

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

    For the independent method to work, you need to find the string of the propagate_sample_times function and manually set the desired calculation step.

    Default cell code:

    function propagate_sample_times(inputs_sample_times::Vector{SampleTime}, fixed_solver::Bool)::SampleTime
    # A function that returns the sampling time of a block.
    # It is used only in the `Custom` inheritance mode.
    # The fixed_solver parameter indicates whether the solver is being used.
    # with a constant step (true) or with a variable step (false).
    # A more complex example of working with time inheritance
    # The block sampling can be viewed in the documentation.
        return first(inputs_sample_times)
    end

    where the calculation step has the structure:

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

    _ Learn more about the definition of a direct end-to-end connection_

    Defines a direct end-to-end connection:

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

    • If this option is selected, a direct end-to-end connection is available. This means that the output signal is controlled directly by the value of the input port.

    Default cell code:

    function direct_feedthroughs()::Vector{Bool}
    # A function that returns an array of Boolean values defining,
    # end-to-end connections. If the ith element of the array is true,
    # the ith port has an end-to-end connection.
    # It is ignored if the default algorithm is used.
        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

In order to find out the types, sizes, and other auxiliary information in the block executable code, use the following constants inside your code Engee Function:

  • `BLOCK_NAME' is the name of the block. Each block added to the Engee canvas has a name that can be accessed through this constant. For example, you can refer to BLOCK_NAME during error initialization to output the block name 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 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 of attributes for each output port. For example, to find out the attributes of the first output signal, use OUTPUT_SIGNAL_ATTRIBUTES[1], where 1 is the first output port of the block. Engee Function.

To find out more information about a specific block port, you can refer to the attributes of its signal by adding a dot . after the constant 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 contact functions:

  • `dimensions' — the dimension of the signal. It can be shortened to `dims'.

  • type — the type of the signal. It can be shortened to `tp'.

  • 'sample_time' is the calculation step. It is a structure similar to the attributes of signals, which can be accessed through a dot. .. Two conversion functions are available:

    • period — the period of the calculation step. The full conversion function is `sample_time.period'. It can be shortened to `St.p'.

    • 'offset' — offset of the calculation step. The full conversion function is sample_time.offset'. It can be shortened to `St.o.

  • direct_feedthrough — indicates whether the loop port opens. It is used only for input ports (checks attributes only for the input port). It can be shortened to `df'.

Model Example Engee Function with all constants and conversion 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

Block Engee Function, and also command prompt img 41 1 2 Engee, supports fixed point (Fixed) and custom bus types (BusSignal). These constructs help to control the behavior of operations when working with integer, real, fixed, and complex data types.

Types and functions of a fixed point (`Fixed-Point')

In the article Fixed-Point Arithmetic (Fixed-Point) in Engee It describes how to work with a fixed point in Engee. The constructions given in the article are also valid for the block Engee Function, this is how the block supports:

  • 'FixedPoint' is the abstract type of all fixed numbers`;

  • Fixed is a specific type of fixed number, created manually with an indication of the bit representation.;

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

  • fixdt(…​) — creating a Fixed type with an indication of the sign, width, and number of fractional bits.

For example:

a = fi(5, 1, 16, 4)     # Signed number, 16 bits, 4 fractional
b = fi(10, 0, 8, 0)     # An unsigned integer of 8 bits
T = fixdt(1, 24, 8)     # Fixed type with 24 bits, 8 of them are fractional
c = Fixed(0x01ff, T)    # Creating a fixed number directly from the bit representation

Fixed numbers in Engee Function may:

Working with custom bus types (`BusSignal')

Custom bus types allow you to specify the types of input and output signals in the form of structured tuples (NamedTuple) with descriptions of names, types, and dimensions inside Engee Function. Available functions:

  • BusSignal{...} is the type of bus with names, types, and dimensions.;

  • get_bus_names(type) — get a list of signal names;

  • get_bus_types(type) — get the types of signals;

  • get_bus_dimensions(type) — get the dimensions of the signals;

  • get_names_types_dims(type) — get everything at once;

  • get_bus_signal_type(::NamedTuple) — determine the type of BusSignal by value.

An example of determining and analyzing the type of tire:

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,), ((), ()))

You can explicitly describe the tires:

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, you can set fixed point values via fi(…​), and also pass the bus structures as `NamedTuple'. The bus type can be determined 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 initialize values. You can also move the main logic here if Step method code or Component struct code are disabled.

  • In the Component structure code cell, when describing the fields of the structure, you can use values of the Fixed type, as well as the BusSignal types, if the fields are composite signals.

  • The Step method code cell implements the main logic of the block. Here, fixed numbers and bus signals can participate in calculations, comparisons, and structure processing.

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

  • In the Dimensions inheritance method cell, you can use the Fixed data and values from the buses to determine the dimensions of the output signals.

  • 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 this state at each step of the simulation.

  • In the Terminate method code cell, these types can be used to record the final state if the structure contains the appropriate fields.

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

Hence, Fixed, fi(…​), fixdt(…​)The `, `BusSignal and get_bus functions are applicable in all aspects of the configuration and operation of the block Engee Function — both at the execution stage and at the generation stage of the type and dimension of the signal. They can be freely used both in the parameters and in the source code in all cells (if they work correctly).

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

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

Available functions:

  • 'econvert' — conversion of values between types;

  • esum, esub, emul, ediv — arithmetic operations with setting the output type;

  • emul! — matrix multiplication with result caching;

  • *_promote_type — output functions based on input types.

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

Supported types

All the functions in this set work with the following types:

  • Real numbers: Float16, Float32, Float64

  • Integers: 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 vectorized expressions using dot syntax (.). Next, let’s look at the functions in more detail.

Converting numbers using econvert

The econvert function allows you to convert a value to a specified type by specifying the rounding method and the overflow handling method.:

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

Here:

  • type — the type to convert the value to. If 'var` is a complex number, the base real type is specified. For example, econvert(Float32, Complex{Int16}(1 + 2im))`returns `Complex{Float32};

  • 'var` — value for conversion;

  • rounding_mode' — rounding method (`RoundDown, RoundUp, RoundNearest, RoundNearestTiesAway, RoundToZero);

  • overflow' — if `true, the overflow behavior (wrap) is used; if false, saturation is enabled — the value is limited to the range of the type.

The 'econvert` function can be used for type conversion before comparisons, inside arithmetic expressions, during signal processing in conditions and other scenarios.

Type-controlled arithmetic using esum, esub, emul, ediv, emul!

Functions of 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 (scalars or arrays are allowed);

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

  • overflow' — enabling overflow (`true) or saturation (false);

  • `rm' is the rounding method (see the list above).

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

The emul!' function is used to speed up when working with matrices: it does not create a new array, but writes the result to an already allocated `cache'. It is important that `eltype(cache)`matched the `out_type'.

The result depends on the type of out_type: it affects not only the final result, but also the behavior of intermediate calculations, which is especially important for Fixed and `Complex'.

Inferring the result type using *_promote_type

If the block parameters (for example, SumType or multitype') are set as the string `"Inherit", the result type is determined automatically based on the input data types using the functions `*_promote_type'.

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

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

    sum_promote_type(::Type{T}, n::Integer=1, hdlsetup::Bool=false)
  • Defines 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)
  • Defines the type of accumulator 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)
  • Defines the type of accumulator when adding an arbitrary number of arguments with different types.:

    sum_accumulator_promote_type_from_inputs(types::Type...; hdlsetup::Bool=false)
  • Defines 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)
  • Defines the output type when dividing a single value of type T:

    div_promote_type(::Type{T}, hdlsetup::Bool=false)
  • Defines 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 take into account the specifics of the hardware platform (`TargetHardware'). By default:

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

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

Usage example

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

example of using 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"

The block code (in the cell Common code, Dimensions inheritance method and Types inheritance method):

  • Cell Common code:

    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 functions econvert, esum, emul, ediv and the rules promote_type allow you to control the types and behavior 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 component behavior tuning using a block. Engee Function.

Diagnostic functions warning, stop_simulation', `pause_simulation', `info

In the source code of the block Engee Function The functions warning, stop_simulation', `pause_simulation and `info' are also available, which allow you to interact with the model diagnostic system at the simulation stage, allowing you to output messages in diagnostic window model diagnosis main. These functions can be used inside the following source code cells:

  • Component struct code

  • Step method code

  • Update method code

  • Terminate method code

  • Common code

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

So, although these functions are formally allowed in the Component struct code cell, in practice their placement there does not make sense: this cell is intended solely to describe the block structure, and not for executable logic. In order for diagnostic functions to work correctly, they must be placed inside the supported simulation methods: Step method, Update method, Terminate method, or in Common code if the corresponding method is redefined there. Example of correct placement:

  • Component struct code:

    #
    struct Block <: AbstractCausalComponent
        g::Real
        function Block()
            new(gain)
        end
    end
  • Common code, the function (c::Block) functor(t::Real, in1) 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 (for example, update!, terminate, step), in the Common code cell manually — if you disable the corresponding standard cell (for example, Define update method, Define step method, etc.).

To work correctly in the Common code cell, the functions warning, stop_simulation, pause_simulation and info can be used:

  • Inside functions that redefine the behavior of methods responsible for executing the model, such as step, update!, terminate!. For example, if the functor normally specified in the Step method code cell is defined in Common code, then the diagnostic functions inside 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 to diagnostic functions inside it will also be performed correctly.

    Redefining 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 to the simulation.

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

# Structure with Component structure code (if disabled, it must be rendered in Common code)
struct Block <: AbstractCausalComponent
    g::Real
    function Block()
        new(gain)
    end
end

# Step method (a mandatory method called at each step of the simulation)
function (c::Block)(t::Real, in1)
    c = update!(c, t, in1)
    return c.g .* in1
end

# The redefined update method!
function update!(c::Block, t::Real, in1)
    if t == 5.0
        info("update triggered at t = $t")  # diagnostic message
    end
    return c
end

As a result, the following messages will be received in the diagnostic window of the model:

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. This can be useful for pointing out non-critical issues or conditions 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 outputs a completion message. It is used to indicate a critical condition under which continued modeling is impossible or undesirable. Example:

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

    engee function continue

    This function can be useful for analyzing the state of the model at a given time. Example:

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

    if t == 5.0 || t == 7.5
        info("time == $t")
    end

Ports

Entrance

Input Port — input port

+ scalar | vector | the 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 — specify the name of the input port. By default, the Label cell is empty (the name is not specified).

  • Type — input signal data type. Choose one of the options:

    • A certain type (all except Inherit') — it is checked that a certain type of signal is being sent 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' (default) — inherits the data type from the associated block. It can be any type of data.

  • Size — the dimension of the input signal:

    • Inheritance of all dimensions (-1 by default) — inherits the dimension of the signal sent 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 two-element signal or (2, 3, 4) for a multidimensional one. If -1 is specified, then the dimension is inherited.

    • Inheriting one of the dimensions — inherits the dimension of the signal sent 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 is set explicitly.

  • Output bus type — input bus type, replaces the dimension of the input signal Size if the data type of the input signal Type has the type BusSignal' (bus). The default value is `BusSignal{(), Tuple{}, ()}'. To block Engee Function I figured out which bus is coming to the input, it’s enough to set Type to the `Inherit value. Explicit type indication is only necessary for the output signal.

    Block Engee Function It does not inherit buses to the output ports, although it can receive them to the inputs. To inherit buses on output ports (for transmission to other blocks), you must explicitly specify the bus type in the Output bus type parameter.
  • Direct feedthrough — defines a direct end-to-end connection:

    • If the checkbox is selected (by default), then a direct end-to-end connection is available. This means that the output signal is controlled directly by the value of the input port.

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

Output

Output port — output port

+ scalar | vector | the 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 — specify the name of the output port. By default, the Label cell is empty (the name is not specified).

  • Type — the data type of the output signal. Choose one of the options:

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

    • `Inherit' (default) — inherits the data type of the output signal. Calculates the smallest common type when there are several input signals of different types. It can be any type of data.

  • Size — the dimension of the output signal:

    • Inheritance of all dimensions (-1 by default) — inherits the dimension of the signal applied to the output port. The output signal will have the dimensions obtained as a result of the broadcast mechanism — Julia will automatically expand the dimension of a smaller data array to a larger dimension in order to correctly inherit the dimension.

    • Defined dimensions — the output 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 two-element signal or (2, 3, 4) for a multidimensional one. If -1 is specified, then the dimension is inherited.

    • Inheriting one of the dimensions — inherits the dimension of the signal sent 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 is set explicitly.

  • Output bus type — the output bus type, replaces the dimension of the input signal Size if the data type of the output signal Type has the type `BusSignal' (bus). The default value is `BusSignal{(), Tuple{}, ()}'. To block Engee Function If you have issued a tire to the outlet, you must explicitly specify its type.

Parameters

Main

Number of input ports — defines the number of input ports

+ 1 (default)

Details

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

Number of output ports — defines the number of output ports

+ 1 (default)

Details

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

Sample time — the interval between the calculation steps
-1 (default)

Details

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

Advanced

Use external cache for non-scalar output — use external cache for non-scalar outputs

+ disabled (by default) | enabled

Details

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

  • If the checkbox is unchecked (by default), the external cache is not used.

  • If the box is checked, the output signal can accept an additional cache argument, which must be taken into account in the source code of EngeeFunctionCode. If the unit has one output port, then in the cell Step method code the cache argument is automatically added to the list of arguments to the function, except when the port dimension is explicitly set as () (scalar). In the source code, it is required to write functors depending on the dimension of the output signal. The functors can be defined in a 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 included in the block parameters.

Sample time inheritance method — definition of the inheritance method of the calculation step
Default (by 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 method of inheritance of the calculation step is determined by the code of the function cell Sample times inheritance method.

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

If the value -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 all other cases (Sample Time is not equal to -1 and SampleTime is greater than or equal to 0) — block Engee Function it works with the specified value of the Sample Time field, ignoring the Sample time inheritance method.
  • Default is the method of inheriting the default calculation step. The Default inheritance method is always used when the block Engee Function it is not discrete or continuous. The method gets any kind of calculation step. When choosing this method, the block Engee Function It will inherit the calculation step according to the following principles:

    • If the block has no input ports, there is a continuous calculation step at the output.

    • If all the calculation steps are the same at the input, the calculation step at the output is the same as the input.

    • If there are continuous calculation steps among the input steps, there are also continuous calculation steps at the output.

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

    • If there is no continuous and fixed number of 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 options is valid.:

      • If the largest common divisor of the discrete calculation steps coincides with one of the input calculation steps or a constant—step solver is used, the output is a discrete calculation step with the step of the largest common divisor.

      • If the variable—step solver and the largest common divisor of the input discrete calculation steps do not match any of the input calculation steps, the output is a fixed small one.

  • Discrete is an inheritance method for obtaining a discrete calculation step. When choosing this method, the block Engee Function It will inherit the calculation step according to the following principles:

    • If there are continuous or fixed small calculation steps at the input, the output is a discrete calculation step with a solver step (even if the solver is with a variable step).

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

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

Example of redefining the propagate_sample_times function with an operation 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 function propagate_sample_times returns '(period = 0 // 1, offset = 0 //1, mode = :Discrete)`, then this calculation step will be perceived as discrete with a solver step.

Setting up the parameters

Number of parameters — specify the number of parameters

+ 1 (default)

Details

The number of parameters used in the block.

Parameter — defines a parameter as a variable

+ scalar | vector | array

Details

Defines a parameter to use in the block source code Engee Function. In the parameter, you can set:

  • `Name' is the name of the parameter.;

  • 'Value' is the value of the parameter. Any Julia expressions can be set as values.

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

Global variables available in the Engee variables window variables icon you can set Parameters tabs as variables for insertion into the block source code. Engee Function. If the name of the parameter and the global variable match, the parameter value will be automatically substituted from the global variable.

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


It is important to distinguish between the block parameters that are set in the Parameters tab and global variables in the source code. Engee Function, from global variables Engee in the variables window variables icon. Although it is possible to use the values and names of global Engee variables in the block parameters, these entities are completely different. Global Engee variables cannot be controlled through block parameters or from its source code. However, for the source code, the block parameters (from the Parameters tab) are global variables and can be used in any part of it without reference to a specific function or block of code.

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

engee function param explain 1

Then the source code with the added parameters 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 customize the behavior of the component. The example uses the block parameter names a_param, b_param, and c_param to set the structure parameters. , , and accordingly. The code also defines the function(c' method.::Block)(t::Real, x::Vector{<:Real}), which scales each element of the vector x by the block parameter , adds and divides the result by . This allows you to flexibly change and normalize the vector x in accordance with the values of the block parameters.


Let’s consider a case where only the parameters of the Parameters tab 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 block of code. This greatly simplifies the code and makes it easy to change the parameters in the Parameters tab without affecting the source code.


Let’s consider the case when the parameters do not fully match the source code. For example, there is a parameter a_param equal to `100'. There is a structure field in the source code :

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 initialize the field. the structure of the struct Block through its constructor, which divides the parameter value by `10'. In this case, the field is returned in the block’s functor .


Variables can be made global right in the source code.:

a = 1;
b = 2;
c = 3;

struct Block <: AbstractCausalComponent; end

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

A Block structure is created in the code and a function is defined that performs mathematical operations with global variables. , and by applying them to the variable x.


If you don’t want to use the parameters from the Parameters tab, you can initialize 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

A Block structure is created in the code and defines a function that performs mathematical operations with local variables. , and by applying them to the variable x.

Let’s consider the most effective and correct way to do it. To block Engee Function it worked correctly, check the box for the option Use external cache for non-scalar output 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("The block parameters must be real")
        all(x->isempty(size(x)), (a_param, b_param, c_param)) ||
          error("The block parameters must be scalars.")
        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 only be written in a cell. Common code, since the standard cells do not allow editing the definition of the component structure and the signature of the functor.

Fields , and structures are block parameters that are strictly checked for types in the constructor. Each of these parameters must be a real scalar (Real), which ensures the accuracy of 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 inappropriate dimensions (they must be scalars), the constructor generates an error with the corresponding message. After verification, the constructor initializes the fields of the structure with the values , and The specified types are Ta, Tb and `Tc'.

The calculated function defined for Block instances takes time t, an external cache, and a vector x of real numbers. Fields are used in this function , and 'Block` structures for calculating values, which are then written to the cache. This avoids unnecessary memory allocations by reusing the provided cache.

Thus, the Block structure ensures strict data type management and efficient use of resources by using an external cache to store the results of calculations.


If you want to change the block parameters, then you need to use the mutable structure. Consider an example:

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

    function Counter()
      isempty(size(limit)) || error("The block limit of $BLOCK_NAME must be a scalar")
isreal(limit) || error("The block limit of $BLOCK_NAME must be a real number")
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 strongly typed. The limit and iter fields of the structure represent the block parameters.:

  • limit is the limit value of the counter.;

  • `iter' is the current value of the counter.

The structure constructor checks (validates) the limit parameter to see if the parameter is a scalar and a real data type. After that, the iter field is initialized with a null value of the corresponding type T'. The `update!' function updates the status of the counter , increasing the value of iter by one with each call. If the current value of `iter' exceeds the `limit', it is reset to zero and allows the counter to cycle back to its initial state.

In the source code of the block Engee Function you can use include, which refers to the external code. This allows you to initialize variables from external code (if any) in the source code.
The actual data type, as well as the support for possible data types, depends on the user code inside the block.

Sample code

This example shows 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 value Discrete for the Sample time inheritance method parameter. Next, 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

As a result, the following source code will be obtained:

engee function example 1

The initial_condition and k parameters are initialized 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 initialized with the value of the initial_condition parameter.

After that, at each calculation step, the block returns the internal state c.state as an output signal and recalculates its value in the update! method.

The component structure is redefined in the cell Common code, and not in Component struct code, since a more flexible definition is required: the structure must be mutable and parameterized by the type T corresponding to the type of state. The standard definition in Component struct code It is suitable only for immutable and nonparametrized structures.

Advanced usage

Block Engee Function allows you to set the behavior of a component using your own code, without having to assemble it from ready-made blocks. This makes it possible to manually control the types and dimensions of the output signals, use caching to improve performance, disable direct transfer of inputs to outputs, and set your own data update period.

So, on the page posted in Engee Community by link provides practical examples of advanced usage of the block Engee Function:

  • Converting input data into a vector with redefinition of output parameters;

  • Using caching and strongly typed structures to improve performance;

  • Implementation of blocks without direct feedthrough for breaking algebraic loops;

  • Setting the user sampling period of the output signal.

Annotations

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

engee function annotations

On this tab:

  • A list of available options (except hidden ones) is displayed on the left.

  • There is a text editor on the right where you can set an annotation with markers in the %<parameter name> format.

  • The parameters can be transferred manually, via auto-completion, or using the button engee function annotations 1.

After exiting the editor (for example, when clicking outside of it), the annotation is applied: the markers are automatically replaced with the actual parameter values, and the final text is displayed under the block name (or above it, if the name is placed on top).

To delete annotations, you must delete the corresponding marker in the editor.

Available Markers

The property marker is automatically replaced with the current parameter value. The following markers are available:

  • Ports: %<Inputs>, %<Outputs> — number of input and output ports; %<InputPort1Type>, %<OutputPort1Type>, %<InputPort1Size>, %<OutputPort1Size> — data type and signal dimension.

  • Time characteristics: %<SampleTime> — discreteness; %<SampleTimeInheritanceMethod> — method of inheritance of discreteness.

  • Code blocks: %<ComponentStructCode>, %<StepMethodCode> — the code of the structure and method of the step.

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

  • Enable flags: %<DefineComponentStruct>, %<UseCommonCode>, %<DefineStepMethod>, %<definupdatemethod>, %<DefineTerminateMethod> — enable the corresponding code sections.

  • Redefinition methods: %<OverrideTypesInhMethod>, %<OverrideDimsInhMethod>, %<OverrideSampleTimeInhMethod> — type inheritance settings.

  • Other: %<UseExternalCache> — external cache usage.