Engee documentation

Working with signals of different dimensions

Vectorisation and broadcast are important techniques for working with models in Engee, allowing you to efficiently handle signals of different dimensions. Using vectorisation, you can perform computations on arrays as a whole, avoiding explicit element loops, which speeds up computation and simplifies code. Broadcast, in its turn, provides automatic matching of dimensions between arrays when performing operations, allowing you to conveniently work with data of different formats.

The use of vectorisation and broadcast in modelling allows efficient processing and analysis of multidimensional data. Vectorisation reduces the number of operations by working with arrays as a whole, and broadcast eliminates the need for manual matching of dimensions.

Vectorisation in modelling

Vectorisation in simulation allows you to evaluate model behaviour for multiple parameter values at once, which is particularly useful in sensitivity analysis and testing. This means that instead of running separate runs for each value, you can specify a matrix of parameters and get results for all values simultaneously.

For example, setting the Amplitude parameter of the Sine Wave Function block to a matrix of values [[1, 2]; [3, 4]] would mean that the block generates multiple signals with the amplitudes specified in the matrix. The image below shows how the Sine Wave block generates several signals with the specified amplitude, which allows you to immediately see the result for different values of the Amplitude parameter:

vectorization x 1

Broadcast

Broadcast allows you to match array dimensions to perform operations on arrays. In Engee, this mechanism automatically extends array dimensions to allow operations on seemingly incompatible arrays. For example, if one array is 2x2 and another is 1x2, then adding the arrays will bring the arrays to compatible dimensions.

Consider an example with two Constant blocks and one Add block. The Constant block with the parameter [3, 4; 5, 6] is added to the Constant block with the parameter [2, 3]. At that broadcast automatically brings arrays to compatible sizes and performs element-by-element addition:

broadcast 1

broadcast 2

The result at the block output will be [5, 7; 7, 9] because the strings have been automatically matched. This approach makes it easy to perform operations between arrays of different sizes and greatly simplifies working with multidimensional signals in modelling.

Custom bus types

Before studying the section on custom tyre types, it is recommended that you first read the blocks Bus Creator and Bus Selector.

In Engee there is support for custom bus data types represented by the BusSignal type. This type allows you to specify a bus structure for blocks that handle such data. Named tuples (NamedTuple) are used as bus objects in Engee, for example:

bus = (s1 = 4, s2 = 5.5, s3 = [1, 2, 3])

The BusSignal type is parameterised by signal names (Names), base types (BaseTypes), and dimensions (Dims). For example, a bus with three signals can be described as follows:

bus_type = BusSignal{(:s1, :s2, :s3), Tuple{Int, Float64, Vector{Int}}, ((), (), (3,))}

here:

  • :s1, :s2, :s3 - signal names;

  • Tuple{Int, Float64, Vector{Int}} - signal data types;

  • (), (), (3,) - dimensions of signals (for example, s3 is an array of length 3).

You can create bus objects through functions or set the bus type as a variable to use it in block settings. To simplify working with bus types, functions that extract names, data types and dimensions are available:

get_names_types_dims(::Type{BusSignal{Names, Types, Dimensions}}) where {Names, Types, Dimensions}

get_bus_names(::Type{BusSignal{Names, Types, Dimensions}}) where {Names, Types, Dimensions}

get_bus_types(::Type{BusSignal{Names, Types, Dimensions}}) where {Names, Types, Dimensions}

get_bus_dimensions(::Type{BusSignal{Names, Types, Dimensions}}) where {Names, Types, Dimensions}

The first function allows you to retrieve information about all bus parameters at once from the bus type, while the next three functions retrieve information about bus names, types and dimensions individually.

Examples of functions
bus_type = BusSignal{(:s1, :s2, :s3), Tuple{Int64, Float64, Int8}, ((), (2,), (2, 2))}
# Экземпляром шины такого типом могла бы выглядеть, например, так: (s1 = 5, s2 = [4.3, 5.4], s3 = Int8[1 2; 3 4])

get_names_types_dims(but_type) # Результат: ((:s1, :s2, :s3), (Int64, Float64, Int8), ((), (2,), (2, 2))) - кортеж из трех кортежей, каждый из которых описывает свой параметр типа шины

get_names(but_type) # Результат: (:s1, :s2, :s3)

get_types(but_type) # Результат: Tuple{Int64, Float64, Int8}, то есть параметр пока что возвращается в исходном виде, что несколько неудобно. В одной из ближайших задач будет исправлено и результатом будет кортеж (Int64, Float64, Int8)

get_dimensions(but_type) # Результат: ((), (2,), (2, 2))

For example, the get_bus_signal_type method converts a named tuple to the appropriate bus type:

bus = (s1 = 5, s2 = [4.3, 5.4], s3 = (a = 4, b = 5.5))
bus_type = get_bus_signal_type(bus)
# bus_type равен BusSignal{(:s1, :s2, :s3), Tuple{Int64, Float64, BusSignal{(:a, :b), Tuple{Int64, Float64}, ((), ())}}, ((), (2,), ())}
Nested structures are also supported, where an element of one bus can be an element of another bus (example above). To specify the type of a nested bus among the types of its elements, you must specify the full type of the nested bus.

Although bus objects in Engee are named tuples, special constructor functions are provided to create them. These functions allow you to create bus objects, ensuring that their structure matches the specified bus type. The following options can be used to create bus objects:

  • With a named tuple:

    BusSignal{Names, Types, Dimensions}(x::NamedTuple{TupleNames, TupleTypes}) where {Names, Types, Dimensions, TupleNames, TupleTypes}
  • With named arguments:

    BusSignal{Names, Types, Dimensions}(kwargs...) where {Names, Types, Dimensions}
  • With the usual motorcade:

    BusSignal{Names, Types, Dimensions}(x::Tuple) where {Names, Types, Dimensions}
Example of creating a bus object

Let’s consider creating a bus object with a type:

bus_type = BusSignal{(:s1, :s2), Tuple{Int64, Float64}, ((), ())}
  • For a named tuple:

    bus1 = bus_type((s1 = 5, s2 = 6.4))

    A named tuple is passed here, the structure of which is fully consistent with bus_type.

  • For named arguments:

    bus2 = bus_type(s1 = 5, s2 = 6.4)

    Here the bus structure is created from named arguments. The names of the signals (s1, s2) and their types must match the definition of bus_type.

  • For a regular tuple:

    bus3 = bus_type(5, 6.4)

    Here the arguments are passed in the order defined by the bus_type. The number, types and sizes of the arguments must correspond to the bus parameters.

Thus, in each example, a bus will be created and of the same bus_type. However, consideration should be given to:

  • For bus1 and bus2, the names, types and sizes of the signals match the bus parameters;

  • For bus3, the number, types and sizes of the arguments passed match the requirements of the bus type.


For blocks such as Constant, you can specify the bus type value via the Output data types parameter, then specify its structure in Output bus type.

user bus signal

For example, let the bus value bus_value = (a = 10, b = [2.5, 3.5], c = (x = 1, y = 2)). Then its type is specified as:

bus_type = BusSignal{(:a, :b, :c), Tuple{Int, Vector{Float64}, NamedTuple{(:x, :y), Tuple{Int, Int>>}, ((), (2,), ((), ())}}