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 ton. -
check_positive(x)checks if the number is positive and returnstrueorfalse.
-
-
In the construction
@parameters:-
a[:] = make_vector(3)— parameteragets an array[1, 2, 3]. The array type is set in square brackets[:]. -
flag::Bool = check_positive(-5)— parameterflaggets the verification resultfalse, and the type is explicitly specified asBool.
-
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 < 10Theny = x; -
If
x >= 10Theny = -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 changingx. -
Functions
gt,lt,ge,le,eq,neqthey allow you to write conditions without detecting events. -
The number of equations in each branch must match. Checks
assertthey 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_parametersthe parameter is declaredn = 3. It determines the dimension of the array. -
In the construction
@variablesA vector stress variable with dimension is declaredn. -
In the construction
@nodesan array is being createdpinsof the 3 electrical ports (Pin). Each element of the array corresponds to a separate port, and the port names will bepins_1, pins_2, … pins_n. -
In the construction
@equationsusing the cycle, an array of equations is formed: voltagev[i]it communicates with the voltage of the corresponding portpins[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.