Документация Engee

Перечисления, ветвления, циклы и модули языка физического моделирования Engee

Страница в процессе разработки.

В этой статье собраны конструкции языка физического моделирования Engee для управления логикой и структурой компонентов: перечисления, ветвления (статические и динамические), циклы, модульная организация кода, функции и настройка внешнего вида блоков через иконки.

Перечисления

Перечисления используются, когда параметр должен принимать одно из заранее определенных значений. Они удобны для задания режимов работы, выбора моделей или конфигураций компонента.

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

В примере создается перечисление CustomEnum с тремя возможными вариантами:

  • CustomEnum.first — значение 1, отображается как "One";

  • CustomEnum.second — значение 2, отображается как "Two";

  • CustomEnum.third — значение 3, отображается как "Three".

Чтобы параметр отображался в интерфейсе Engee (окне настроек блока) как выпадающий список с вариантами перечисления, нужно явно указать его тип:

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

Здесь параметр math_model имеет тип CustomEnum и по умолчанию равен CustomEnum.first. В интерфейсе Engee такой параметр будет представлен выпадающим списком, где доступны только значения из перечисления.

Типы результатов функций

Иногда параметры или переменные получают значения не напрямую, а через вызов пользовательской функции. Если такая функция возвращает массив, логическое значение (Bool) или перечисление (Enum), то для переменной или параметра обязательно нужно указать явный тип.

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

Здесь:

  • Перед компонентом объявлены две функции:

    • make_vector(n) создает массив чисел от 1 до n.

    • check_positive(x) проверяет, является ли число положительным и возвращает true или false.

  • В конструкции @parameters:

    • a[:] = make_vector(3) — параметр a получает массив [1, 2, 3]. Тип массива задан квадратными скобками [:].

    • flag::Bool = check_positive(-5) — параметр flag получает результат проверки false, и тип явно указан как Bool.

Без явного указания типов (например, [:] для массива или ::Bool для логической переменной) модель не будет работать корректно.

Ветвления

Ветвление — это способ описать разные варианты поведения компонента в зависимости от значения параметров. Внутри ветвлений задается правило: «если выполняется условие — используется один блок кода, иначе — другой».

В языке физического моделирования Engee ветвления бывают двух видов:

  • Статические ветвления (if … elseif … else … end) — выполняются один раз при запуске модели. Они зависят только от параметров. Такой код разворачивается на этапе сборки модели и не меняется во время моделирования. Применяются для выбора структуры, формул или числа элементов еще до запуска расчетов.

  • Динамические ветвления (ifelse(…​)) — проверяют условия и меняют результат в процессе расчета. Ветка выбирается в зависимости от текущих значений переменных, и результат может меняться на каждом шаге интеграции. Такие ветки позволяют описывать режимы работы, которые переключаются «на лету».

В итоге статические ветвления нужны для настройки модели перед запуском, а динамические — для описания поведения системы во времени.

Статические ветвления

Статические ветвления применяются, когда условие зависит от значения параметров. Ветка выбирается один раз при запуске модели и дальше не меняется.

@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

Условие has_option и model == Model.simple отрабатывает при запуске модели и выбирает соответствующую систему уравнений для переменной x.

Использовать if … elseif … else при наличии переменных в условиях вне блока @equations нельзя — такие ветки фиксируются по начальному значению и не меняются во время моделирования. Для переменных применяйте ifelse(…​) или if …​ elseif …​ else внутри блока @equations.

Динамические ветвления

Динамические ветвления работают во время моделирования и зависят от значений переменных. Для этого используется функция ifelse(condition, expr1, expr2). Она позволяет описывать переключение уравнений «на лету». Пример:

@variables begin
    x = 0
    y = 0
end

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

Здесь:

  • Если x < 10, то y = x;

  • Если x >= 10, то y = -x.

В отличие от статического if, такое условие проверяется на каждом шаге моделирования.

При использовании ifelse включено обнаружение событий: решатель пытается как можно точнее определить момент, когда результат условия изменяется с true на false и наоборот. В этот момент происходит реинициализация системы.

Чтобы избежать обнаружения событий, используют специальные функции:

  • gt — больше;

  • lt — меньше;

  • ge — больше или равно;

  • le — меньше или равно;

  • eq — равно;

  • neq — не равно.

Используйте ifelse(…​), когда нужен точный момент переключения состояния, и gt/lt/ge/le/eq/neq, когда важно избежать событий и лишних пересчетов системы.

Использование ветвления if …​ elseif …​ else …​ end внутри конструкции @equations эквивалентно функции 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) переключает выражение при изменении x.

  • Функции gt, lt, ge, le, eq, neq позволяют писать условия без обнаружения событий.

  • Количество уравнений в каждой ветке должно совпадать. Проверки assert не учитываются при проверке баланса.

Циклы

Циклы позволяют сразу объявлять несколько портов, подкомпонентов, уравнений, что удобно для массивов однотипных элементов. Пример:

@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

Здесь:

  • В конструкции @structural_parameters объявляется параметр n = 3. Он определяет размерность массива.

  • В конструкции @variables объявляется векторная переменная напряжениями с размерностью n.

  • В конструкции @nodes создается массив pins из 3 электрических портов (Pin). Каждый элемент массива соответствует отдельному порту, а имена портов будут pins_1, pins_2, …​ pins_n.

  • В конструкции @equations с помощью цикла формируется массив уравнений: напряжение v[i] связывается с напряжением соответствующего порта pins[i].v.

Так, одним блоком кода описано сразу 3 одинаковых соединений. Если изменить значение n, то количество портов и уравнений автоматически изменится.

Массивы подкомпонентов

Тот же прием можно использовать и для подкомпонентов. Как мы только что сделали массив портов и уравнений, так же можно создавать массивы из однотипных компонентов. Это позволяет, например, быстро собрать схему из нескольких резисторов, конденсаторов или диодов.

@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

Здесь создается:

  • Массив из 5 резисторов;

  • Массив из 3 конденсаторов.

Снаружи элементы будут иметь имена res_1, res_2, … и cap_1, cap_2, … . К каждому элементу можно обращаться по индексу, а также использовать их в соединениях и уравнениях.

Циклы в @components работают по тому же принципу, что и для портов и уравнений: один шаблон — несколько экземпляров.

Векторные уравнения

Часто удобно задавать уравнения в векторной форме, а не по одному. Пример:

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

@equations begin
    x_vector ~ c
end

Такое объявление равносильно трем уравнениям: x_vector[1] ~ c[1], x_vector[2] ~ c[2], и т.д.

Также поддерживается точечный синтаксис для поэлементных операций с массивами:

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

Здесь в x_vector .~ c .* 2 каждый элемент x_vector равен соответствующему элементу c умноженному на 2, а в y .~ sin.(u .+ v) происходит поэлементное сложение массивов u и v с последующим вычислением синуса для каждого элемента.

Модульная структура

Компоненты, коннекторы и перечисления можно объединять в модули. Модули позволяют группировать связанные элементы, а также экспортировать их и использовать в других частях проекта по полному квалифицированному пути, также как в обычном коде Julia.

Иконки

Внешний вид блока можно настраивать с помощью конструкции @icon. В качестве иконки можно использовать svg картинки. Пример:

@icon begin
    "icon.svg"
end

Также поддерживается сокращённая запись без begin …​ end:

@icon "icon.svg"