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

Структурные элементы и управление логикой декларативного языка Engee

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

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

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

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

Подробный разбор перечислений, синтаксиса и примеров см. в статье Типизация и единицы измерения.

Ветвления

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

В декларативном языке 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 model == Model.simple
        @equations begin
            x ~ 1 * t
        end
    else
        @equations begin
            x ~ 2 * t
        end
    end

    @equations begin
        y ~ if default(has_option) 2 * x else 5 * x end
    end
end
  • Условие model == Model.simple отрабатывает на этапе объявления.

  • Внутри @equations используется default(has_option), чтобы корректно подставить параметр в условие.

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

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

Динамические ветвления работают во время моделирования и зависят от значений переменных. Для этого используется функция 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, такое условие пересчитывается на каждом шаге моделирования.

При использовании обычных сравнений (x < 10) такие ветки порождают события (момент переключения фиксируется отдельно). Чтобы задать ветвления без событий, применяйте специальные функции gt, lt, ge, le, eq, neq.

Ветвления в уравнениях

Для выбора, зависящего от переменных во времени, используют функцию ifelse(condition, a, b). Она позволяет корректно переключать выражения во время моделирования.

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

    # Сравнение без события
    if gt(x, 0.5)
        u ~ 1
    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 позволяют писать условия без событий.

  • Количество уравнений в каждой ветке должно совпадать.

Циклы

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

@structural_parameters begin
    n::Int = 5
end

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

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

Здесь:

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

  • В секции @components создается массив pins из 5 электрических портов (Pin). Каждый элемент массива соответствует отдельному порту.

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

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

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

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

@engeemodel ArrayExample begin
    @components begin
        diode[1:5] = Diode()
        cap[1:3] = Capacitor()
    end
end

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

  • Массив из 5 диодов (diode[1:5]);

  • Массив из 3 конденсаторов (cap[1:3]).

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

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

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

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

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

@equations begin
    x_vector ~ rhs
end

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

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

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

module Electrical
    @engeeconnector Pin begin
        ...
    end

    module Enumerations
        @descriptive_enumeration Type begin
            first  = 1, "One"
            second = 2, "Two"
        end
    end
end

@engeemodel begin
    @components begin
        pin = Electrical.Pin()
    end
    @parameters begin
        type::Type = Type.first
    end
end

Здесь:

  • Объявлен модуль Electrical с коннектором Pin и перечислением Type;

  • Далее используется компонент, в котором подключается Electrical.Pin() и параметр с типом Type.

Иконки

Внешний вид блока можно настраивать с помощью конструкции @icon. Она задает описание иконки через словарь Dict, что позволяет управлять формой, текстом и другими графическими элементами. Пример:

@icon begin
    rectangle("Resistor")
end

Краткие рекомендации

  • Используйте if … elseif … else для параметров и структурных решений.

  • Для переменных во времени применяйте ifelse(…​).

  • Внутри секций пишите условия по параметрам через default(…​).

  • Чтобы избежать лишних событий, применяйте gt/lt/ge/le/eq/neq вместо обычных сравнений.

  • Масштабируйте код массивами подкомпонентов и векторной записью уравнений.