Продвинутая разработка блоков Engee
Продвинутая разработка блоков на основе Engee Function
Описание проекта
Мы уже научились разрабатывать свои блоки при помощи Engee Function.
А что делать, если алгоритм, который надо реализовать, сложный и громоздкий? Сразу возникают проблемы:
-
Код Engee Function получается раздутый и его попросту тяжело читать
-
Непонятно, как тестировать такой код. Если возникает ошибка, то локализовать ее сложно
Решение этих проблем - это не "ракетная наука", а рутинная работа для программистов. Поэтому давайте побудем немного ими!
Вот как мы будем решать проблемы громоздкого кода:
-
Выделим алгоритм в отдельный модуль
-
Покроем модуль тестами
-
Повысим устойчивость кода
В качестве примера рассмотрим алгоритм нахождения расстояния между двумя наборами наблюдений. А для работы нам потребуются следующие пакеты:
import Pkg
Pkg.add(["LinearAlgebra", "Test", "BenchmarkTools"])
Вынос кода в модуль
Модуль в контексте Julia - это код, который заключен в отдельное пространство имен. Это позволяет делать переменные внутри этого модуля "невидимыми" вне его. Чтобы подробнее узнать о преимуществах модулей обратитесь к справке.
Посмотрим на код нашего алгоритма, заключенного в модуль:
;cat PDIST2.jl
Тестирование
Цель тестирования - доказать, то что код работает правильно, ловятся все исключения.
Тесты пишутся с помощью пакета Test.jl
Создадим три простых теста:
-
Простой вызов функции
-
Обработка разных размерностей
-
Корректность вычисления дистанций между одинаковыми матрицами
Поместим эти тесты в набор тестов, который задается как:
@testset <имя_набора> begin
<тесты>
end
Особенность тестов в пакете Test.jl заключается в том, что макросы тестирования проверяют истинность некоего выражения. Посмотрим на пример:
using Test, LinearAlgebra
demoroot = @__DIR__
include("PDIST2.jl")
X = rand(3,3)
Y = rand(3,3)
Z = PDIST2.EF_pdist2(X,Y);
@testset "EF_pdist2 tests" begin
@test_nowarn PDIST2.EF_pdist2(X,Y);
@test_throws DimensionMismatch PDIST2.EF_pdist2(rand(3,3),rand(2,2))
@test iszero(diag(PDIST2.EF_pdist2(X,X)))
end
Оценка производительности
Чтобы оценить общую производительность кода требуется замерять несколько показателей:
-
Скорость исполнения
-
Количество выделяемой памяти
-
Количество аллокаций (специфично для Julia)
Для этого будем использовать пакет BenchmarkTools.jl. Его преимущество заключается в том, что можно как тонко настроить эксперимент, так и сразу приступить к замерам:
using BenchmarkTools
@benchmark PDIST2.EF_pdist2(rand(10,10),rand(10,10))
Для нас важны следующие измерения: Time и Memory estimate.
Time - это время выполнения одного прогона. Так как @benchmark запускает несколько прогонов, то мы получаем набор таких измерений и можем применить к нему статистическую обработку. Ее результаты показываются после запуска макроса @benchmark, как показано выше
Memory Estimate покажет объем выделяемой памяти. Давайте посмотрим, как растет время исполнения и объем памяти по мере увеличения объема входа:
matrix_size = 80 # @param {type:"slider",min:10,max:100,step:10}
@benchmark PDIST2.EF_pdist2(rand(matrix_size,matrix_size),rand(matrix_size,matrix_size))
Устойчивость к отказам
Создаваемый код должен быть устойчивым к отказам. Для этого требуется работать с исключениями.
Исключение, в общем смысле - это какая-либо ошибка, которую можно обработать. Главное отличие от обычных ошибок (синтаксических) - то, что пользователь сам может генерировать исключения. Посмотрим на код нашей функции pdist2:
m, n = size(X)
p, n2 = size(Y)
n == n2 || throw(DimensionMismatch("Number of columns in X and Y must match"))
Если размерности матриц не совпадают, то вызывается функция throw и "бросается" исключение.
Исключение можно обработать при помощи конструкции
try
catch
end
в блок try помещается вызов какого-либо кода. Если этот код порождает исключение, то будет выполнен код из блока catch.
Использование библиотеки в Engee Function
В качестве примера будем использовать модель EF_dist_find:
mdl = engee.open(joinpath(demoroot,"EF_dist_find.engee"));
Рассмотрим конструктор блока PDIST2:
include("/user/start/examples/base_simulation/advanced_block_development/PDIST2.jl")
mutable struct Block <: AbstractCausalComponent
cache::Matrix{Float64};
function Block()
c = zeros(Float64,INPUT_SIGNAL_ATTRIBUTES[1].dimensions);
info("Allocated $(Base.summarysize(c)) bytes for pdist2")
new(c)
end
end
Мы включаем наш модуль в Engee Function, указывая полный путь до его кода.
При помощи функции info() будем выводить количество памяти, выделяемой при создании кеша.
Рассмотрим метод Step этого блока:
function (c::Block)(t::Real, in1, in2)
try
c.cache = PDIST2.EF_pdist2(in1,in2);
catch
error("Matrix Dimensions should be equal!")
stop_simulation()
end
return c.cache
end
Обратим внимание на то, что вызов функции из нашего модуля обернут в обработчик исключений. Если наша функция pdist2 породит исключение, то в окне диагностики появится ошибка "Matrix Dimensions should be equal!", а симуляция будет остановлена.
При открытии создаются две матрицы 3x3 со случайными числами. Убедимся в том, что модель работает:
engee.run(mdl)
Выводы
В проекте был показан подход к созданию блока на основе Engee Function, обеспечивающий простоту отладки и повышающий качество и надежность кода.