Продвинутые настройки блока Engee Function¶
Блок Engee Function позволяют пользователю встроить свой алгоритм в модель Engee, не перерисовывая его на примитивах. При этом пользователь обладает полным контролем определения поведения блока.
Ниже приведены примеры методов такого контроля.
Перегрузка параметров выхода¶
Пример: Приведение к вектору¶
Требуется реализовать блок, который преобразует свой вход в вектор.
Никаких операций над входом не требуется. Единственное, что требуется - преобразовать вход в вектор. Для этого необходимо вычислить длину вектора и получить тип его элементов.
Данная задача реализована в модели vectorize_it блоком VectorizeIT.
Рассмотрим код этой Engee Function. Нас интересует функтор блока и метод переопределения размерностей и типа выхода.
Начнем с определения функтора:
function (c::Block)(t::Real, cache, in1)
if in1 isa AbstractVector
cache .= in1
elseif in1 isa AbstractArray
cache .= vec(in1)
elseif in1 isa Real
cache[1] = in1
end
end
Здесь следует обратить внимание на переменную cache. Эта переменная объявляется для блока Engee Function, если включена настройка Use external cache for non-scalar output
:
Эта настройка доступна только если у блока есть только один выход.
Далее, рассмотрим определение размерности и типа выходного сигнала. Можно определять их по отдельности, выбрав опции Override type inheritance method и Override dimensions inheritance method. Однако, можно определить их в единой функции. Для этого требуется выбрать Use common method for types and dimensions inheritance (выбор станет доступным после включения обоих методов):
Для данного случая наиболее уместен последний вариант. Рассмотрим код этого метода:
function propagate_types_and_dimensions(inputs_types::Vector{DataType}, inputs_dimensions::Vector{<:Dimensions})::Tuple{Vector{DataType}, Vector{<:Dimensions}}
in1t = inputs_types[1]
in1d = inputs_dimensions[1]
outd = (prod(in1d), )
return ([eltype(in1t)], [outd]);
end
На вход поступает два вектора: типы входов и их размерности. На выходе должен быть сформирован кортеж состоящий из двух векторов:
- вектор из типов данных выходных сигналов
- вектор из размерностей выходных сигналов
Следует отдельно указать на то, что размерность передается как кортеж (то есть так же как результат функции size()
), а тип данных как тип элемента. В нашем случае это будет не Vector{eltype(in1t)}
, а просто eltype(in1t)
.
Строгая типизация блока и кеширование результатов¶
Пример: Вычисление системы уравнений¶
Язык Julia позволяет динамически выделять память, но это приводит к замедлению кода. Поэтому блок Engee Function может оказать негативное влияние на скорость симуляции. В данном примере продемонстрировано как добиться максимальной производительности блока Engee Function.
Использование отдельной переменной для хранения результата работы блока (кеша) позволяет избавиться от динамического выделения памяти и существенно ускоряет работу блока. Рассмотрим простой пример:
a = [[i, 2i, 3i] for i in 1:1_000_000]
b = [[4i, 5i, 6i] for i in 1:1_000_000]
cache = [0, 0, 0]
function sum_without_cache(a, b)
@time for i in eachindex(a)
a[i] .+ b[i]
end
end
function sum_with_cache(cache, a, b)
@time for i in eachindex(a)
cache .= a[i] .+ b[i]
end
end
println("Without cache:")
sum_without_cache(a, b)
println("With cache:")
sum_with_cache(cache, a, b);
Как видно из примера, применения кеша является крайне желательной практикой. Для Engee Function мы можем определить кеш двумя способами:
- Указав чекбокс Use external cache for non-scalar output (для одного выхода)
- Вручную создав кеши в определении блока
Для достижения максимальной производительности, все поля структуры блока должны иметь конкретный тип и фиксированный размер.
В качестве примера рассмотрим модель cached_calcs и блок ComputingTypesRuntime.
Этот блок реализует следующую систему уравнений: $$ \begin{cases} y_1 = sin(u_1) + cos(u_2)\\ y_2 = u_1 + u_2 \end{cases} $$
Реализация этой системы подразумевает использование кешей, так как входы могут быть нескалярными.
Что бы создать кеши нам требуется определить типизированную структуру. Обычный метод Define component structure не позволяет сделать этого, однако мы можем включить настройку Use common code и создать структуру для блока в "общем" коде. Код определения структуры блока будет следующим:
struct Block{CacheType1,CacheType2} <: AbstractCausalComponent
c1::CacheType1;
c2::CacheType2;
function Block()
sz1 = OUTPUT_SIGNAL_ATTRIBUTES[1].dimensions;
sz2 = OUTPUT_SIGNAL_ATTRIBUTES[2].dimensions;
type1 = OUTPUT_SIGNAL_ATTRIBUTES[1].type;
type2 = OUTPUT_SIGNAL_ATTRIBUTES[2].type;
c1 = isempty(d1) ? zero(t1) : zeros(t1, d1)
c2 = isempty(d2) ? zero(t2) : zeros(t2, d2)
new{typeof(c1), typeof(c2)}(c1,c2);
end
end
Следует отметить то, что тип и размерность кешей выводятся из выходных сигналов. А атрибуты выходных сигналов выводятся в методе Use common method for types and dimensions inheritance при помощи однократного вычисления системы уравнений:
function propagate_types_and_dimensions(inputs_types::Vector{DataType},
inputs_dimensions::Vector{<:Dimensions})
::Tuple{Vector{DataType}, Vector{<:Dimensions}}
in1t = inputs_types[1]
in2t = inputs_types[2]
in1d = inputs_dimensions[1]
in2d = inputs_dimensions[2]
mockin1 = zeros(in1t, in1d)
mockin2 = zeros(in2t, in2d)
mockout1 = sin.(mockin1) .+ cos.(mockin2)
mockout2 = mockin1 .+ mockin2
return ([eltype(mockout1), eltype(mockout1)], [size(mockout1), size(mockout1)])
end
Такой прием приведет только к однократному выделению памяти.
Direct feedthrough¶
Пример: Разрыв алгебраической петли¶
В качестве примера рассмотрим практическую задачу по разрыву алгебраической петли. Рекомендуется разрывать алгебраическую петлю с помощью блока задержки. Но этот прием приводит к искажению результатов. А блок IC не разрывает петлю.
Поэтому, создадим такой блок Engee Function, который бы реализовывал следующую систему уравнений и разрывал алгебраическую петлю: $$ y(t) = \begin{cases} IC, t = 0\\ u, t > 0 \end{cases} $$ Для разрыва алгебраической петли нам требуется снять атрибут direct feedthrough. Это можно сделать как и из настроек блока Engee Function, так и программно, поставив опцию Override direct feedthrough setting method.
Реализация примера доступна в модели loopbreaker в блоке LoopBreaker.
Принцип дейстивия блока следующий: задается параметр IC
, который будет выдаваться на выход в момент инициализации модели, и в начальный момент времени симуляции. На следующих шагах симуляции вход блока будет сразу передаваться на выход. Посмотрим на реализацию этого алгоритма:
Блок определяется как:
struct Block{T} <: AbstractCausalComponent
InCn::T
function Block()
InCn_t = INPUT_SIGNAL_ATTRIBUTES[1].type;
new{InCn_t}(IC)
end
end
А его функтор как:
function (c::Block)(t::Real, in1)
if t<=0.0
return c.InCn
else
return in1
end
end
Дополнительно определен метод Override direct feedthrough setting method. Здесь не требуется никакой логики или вычислений, поэтому мы просто размыкаем петлю:
function direct_feedthroughs()::Vector{Bool}
return [false]
end
Удостоверимся в том, что симуляция работает корректно:
demoroot = @__DIR__
mdl = engee.load(joinpath((demoroot),"loopbreaker.engee");force=true);
simres = engee.run(mdl)
st = collect(simres["Ref.1"])
res = collect(simres["LoopBreaker.1"])
using Plots
p = Plots.plot(st.time, st.value, label="Step")
Plots.plot!(res.time, res.value, label="LoopBreaker")
Plots.plot!(legend=:topleft)
engee.close(mdl;force=true)
display(p)
Настройка Sample Time¶
Пример: "Замедление" выхода¶
Возможны такие модели и алгоритмы, в которых требуется обеспечивать определенный период обновления выходов блоков. В случае блока Engee Function у пользователя есть два метода реализации такого требования:
- явное указание периода дискретизации через параметры блока
- Написание своего алгоритма определения необходимого периода
В данном примере рассмотрен второй вариант.
Рассмотрим модель sample_time_override:
Функционально блоки Engee Function ST_Orig и ST_Override соответствуют алгоритму из раздела Кешей и строгой типизации. Однако, теперь появилось требование выдавать результат с наиболее низким периодом. Рассмотрим, как работает встроенный механизм наследования периода дискретизации:
Видно, что наследуется период D1, то есть самый высокий период. Но задача стоит в наследовании D2. Для этого в коде Engee Function включим секцию Override sample time inheritance method, и выберем наиболее медленный период:
function propagate_sample_times(inputs_sample_times::Vector{SampleTime},
fixed_solver::Bool)::SampleTime
max_period = argmax(t -> t.period, inputs_sample_times)
return max_period
end
После имплементации этого метода получается следующий результат:
Видно, что теперь наследуется нужный период, D2
ВАЖНО!¶
Первый аргумент - это вектор именованных кортежей типа SampleTime, задающийся как:
SampleTime = NamedTuple{(:period, :offset, :mode), Tuple{Rational{Int64}, Rational{Int64}, Symbol}}
Поле mode этого кортежа принимает следующие значения: :Discrete, :Continuous, :Constant, :FiM.
Второй аргумент является флагом, который показывает класс решателя. Если флаг равен true, то выбран решатель с постоянным шагом.
Выводы¶
Были рассмотрены методы продвинутого программирования блока Engee Function. Данные методы позволяют детально определить требуемое поведение блока и добиться максимальной производительности. В данном материале не рассмотрены типичные ошибки при написании кода блоков и приемы написания безопасного кода.