Engee documentation

Enumerations, branches, loops, and modules of the Engee Physics Modeling Language

This article contains the constructs of the Engee physical modeling language for controlling the logic and structure of components: enumerations, branches (static and dynamic), loops, modular code organization, functions, and customizing the appearance of blocks through icons.

Transfers

Enumerations are used when a parameter must take one of the predefined values. They are convenient for setting operating modes, selecting models or configurations of a component.

@descriptive_enumeration CustomEnum begin
    first  = 1, "One"
    second = 2, "Two"
    third  = 3, "Three"
end

In the example, an enumeration is created CustomEnum with three possible options:

  • CustomEnum.first — the value of 1 is displayed as "One";

  • CustomEnum.second — the value of 2 is displayed as "Two";

  • CustomEnum.third — the value 3 is displayed as "Three".

In order for the parameter to be displayed in the Engee interface (the block settings window) as a drop-down list with enumeration options, you must explicitly specify its type.:

@engeemodel CustomComponent begin
    @parameters begin
        math_model::CustomEnum = CustomEnum.first
    end
end

Here is the parameter math_model has a type CustomEnum and by default it is equal to CustomEnum.first. In the Engee interface, such a parameter will be represented by a drop-down list, where only the values from the enumeration are available.

Types of function results

Sometimes parameters or variables do not get their values directly, but through a call to a custom function. If such a function returns an array, the boolean value (Bool) or enumeration (Enum), then an explicit type must be specified for the variable or parameter.

make_vector(n) = [i for i in 1:n]
check_positive(x) = x > 0

@engeemodel Component begin
    @parameters begin
        a[:] = make_vector(3)            # функция вернула вектор [1, 2, 3]
        flag::Bool = check_positive(-5)  # функция вернула false
    end
end

Here:

  • Two functions are declared before the component:

    • make_vector(n) creates an array of numbers from 1 to n.

    • check_positive(x) checks if the number is positive and returns true or false.

  • In the construction @parameters:

    • a[:] = make_vector(3) — parameter a gets an array [1, 2, 3]. The array type is set in square brackets [:].

    • flag::Bool = check_positive(-5) — parameter flag gets the verification result false, and the type is explicitly specified as Bool.

Without explicitly specifying the types (for example, [:] for an array or ::Bool for a logical variable), the model will not work correctly.

Branches

Branching is a way to describe different behaviors of a component depending on the value of the parameters. A rule is set inside the branches: "if the condition is met, one block of code is used, otherwise another is used."

There are two types of branches in the Engee physical modeling language.:

  • Static branches (if … elseif … else … end) — are executed once when the model is started. They depend only on the parameters. This code is deployed at the model assembly stage and does not change during modeling. They are used to select the structure, formulas, or number of elements before starting calculations.

  • Dynamic branching (ifelse(…​)) — check the conditions and change the result during the calculation process. The branch is selected depending on the current values of the variables, and the result may change at each integration step. Such branches allow you to describe operating modes that switch on the fly.

As a result, static branches are needed to configure the model before launch, and dynamic branches are needed to describe the behavior of the system over time.

Static branches

Static branches are used when the condition depends on the value of the parameters. The branch is selected once when the model is started and does not change further.

@descriptive_enumeration Model begin
    simple = 1, "Simple"
    adv    = 2, "Advanced"
end

@engeemodel Example begin
    @parameters begin
        has_option::Bool = true
        model::Model = Model.simple
    end
    @variables begin
        x = 0
        y = 0
    end

    if has_option
        @equations begin
            y ~ 2 * x
        end
    else
        if model == Model.simple
            @equations begin
                y ~ 5 * x
            end
        else
            @equations begin
                y ~ 5 * x^2
            end
        end
    end
end

Condition has_option and model == Model.simple it works out when starting the model and selects the appropriate system of equations for the variable x.

Use if … elseif … else if there are variables in the conditions outside the block @equations it is impossible - such branches are fixed by the initial value and do not change during the simulation. For variables, apply ifelse(…​) or if …​ elseif …​ else inside the block @equations.

Dynamic branching

Dynamic branches work during simulation and depend on the values of variables. To do this, use the function ifelse(condition, expr1, expr2). It allows you to describe switching equations on the fly. Example:

@variables begin
    x = 0
    y = 0
end

@equations begin
    y ~ ifelse(x < 10, x, -x)
end

Here:

  • If x < 10 Then y = x;

  • If x >= 10 Then y = -x.

As opposed to static if such a condition is checked at each step of the simulation.

When using ifelse event detection is enabled: the solver tries to determine as accurately as possible the moment when the result of the condition changes from true on false and vice versa. At this point, the system is being reinitialized.

Special functions are used to avoid event detection.:

  • gt — more;

  • lt — less;

  • ge — greater than or equal to;

  • le — less than or equal to;

  • eq — equal to;

  • neq — not equal.

Use ifelse(…​) when the exact moment of state switching is needed, and gt/lt/ge/le/eq/neq when it is important to avoid events and unnecessary recalculations of the system.

Using branching if …​ elseif …​ else …​ end inside the structure @equations equivalent to the function ifelse:

@equations begin
    # Переключение по условию с событием
    y ~ ifelse(x < 10, x, -x)

    # Сравнение без события
    if gt(x, 0.5)
        u ~ 1
        v ~ 2
    elseif x < 0           # это условие создаст событие
        if lt(y, 1)        # это условие не создаст событие
            u ~ 3
        else
            u ~ 4
        end
        v ~ 5
    else
        u ~ 6
        v ~ 7
    end
end
  • ifelse(x < 10, x, -x) switches the expression when changing x.

  • Functions gt, lt, ge, le, eq, neq they allow you to write conditions without detecting events.

  • The number of equations in each branch must match. Checks assert they are not taken into account when checking the balance.

Cycles

Loops allow you to declare multiple ports, sub-components, and equations at once, which is convenient for arrays of the same type of elements. Example:

@structural_parameters begin
    n::Int = 3
end

@variables begin
    v[:] = zeros(n)
end

@nodes begin
    pins = [EngeePhysicalFoundation.Electrical.Pin() for i in 1:n]
end

@equations begin
    [v[i] ~ pins[i].v for i in 1:n]
end

Here:

  • In the construction @structural_parameters the parameter is declared n = 3. It determines the dimension of the array.

  • In the construction @variables A vector stress variable with dimension is declared n.

  • In the construction @nodes an array is being created pins of the 3 electrical ports (Pin). Each element of the array corresponds to a separate port, and the port names will be pins_1, pins_2, …​ pins_n.

  • In the construction @equations using the cycle, an array of equations is formed: voltage v[i] it communicates with the voltage of the corresponding port pins[i].v.

So, one block of code describes three identical connections at once. If you change the value n, the number of ports and equations will automatically change.

Arrays of sub-components

The same technique can be used for sub-components. Similar to arrays of ports and equations, arrays of the same type of components can be created. This allows, for example, to quickly assemble a circuit from several resistors, capacitors, or diodes.

@engeemodel ArrayExample begin
    @components begin
        res = [EngeePhysicalFoundation.Electrical.Elements.Resistor() for i in 1:5]
        cap = [EngeePhysicalFoundation.Electrical.Elements.Capacitor() for i in 1:3]
    end
end

It is being created here:

  • An array of 5 resistors;

  • An array of 3 capacitors.

On the outside, the elements will have names res_1, res_2, …​ and cap_1, cap_2, … . Each element can be accessed by an index, as well as used in compounds and equations.

Cycles in @components they work on the same principle as for ports and equations: one template - multiple copies.

Vector equations

It is often convenient to set equations in vector form rather than one at a time. Example:

@variables begin
    x_vector[:] = [0, 0, 0]
    c[:] = [1, 2, 3]
end

@equations begin
    x_vector ~ c
end

Such a declaration is equivalent to three equations: x_vector[1] ~ c[1], x_vector[2] ~ c[2], and so on .

Dot syntax is also supported for element-wise operations with arrays.:

@equations begin
    x_vector .~ c .* 2
    y .~ sin.(u .+ v)
end

Here in x_vector .~ c .* 2 each element x_vector is equal to the corresponding element c multiplied by 2, and in y .~ sin.(u .+ v) arrays are added piecemeal. u and v followed by the calculation of the sine for each element.

Modular structure

Components, connectors, and enumerations can be combined into modules. Modules allow you to group related elements, as well as export them and use them in other parts of the project in a fully qualified way, just like in regular Julia code.

Icons

The appearance of the block can be customized using the design @icon. You can use svg images as icons. Example:

@icon begin
    "icon.svg"
end

Shortened recording is also supported without begin …​ end:

@icon "icon.svg"