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

Обработка данных моделирования

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

Введение

Для того, чтобы в процессе симуляции записываемые сигналы модели сразу попали в рабочую область, есть два пути:

  1. Использовать переменную simout
  2. Использовать блоки To Workspace

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

1. Используем simout

1.1. Получение переменной

Чтобы необходимые данные из модели записались в переменную simout, нужно сделать следующее:

  1. Поставить необходимые сигналы на запись.
  2. Включить в настройках модели опцию "Сохранять результаты симуляции в рабочую область".

Рекомендация

Для удобства работы записываемым сигналам рекомендуется определить имя.

Возможность

При установке сигналов на запись и снятия с записи рекомендуемый к использованию инструмент - редактор данных модели.

image.png

Теперь, после выполнения модели мы получаем в рабочей области переменную simout:

In [ ]:
model1_path = "$(@__DIR__)/simulation1.engee" 
engee.load(model1_path) |> engee.run; # загрузили и выполнили модель
engee.close(engee.gcm(), force=true) # закрыли модель

simout
Out[0]:
SimulationResult(
    run_id => 8,
    "simulation1/sin_5Hz" => WorkspaceArray{Float64}("simulation1/sin_5Hz")
,
    "simulation1/sin_2Hz" => WorkspaceArray{Float64}("simulation1/sin_2Hz")
,
    "simulation1/Генератор синусоиды.1" => WorkspaceArray{Float64}("simulation1/Генератор синусоиды.1")
,
    "simulation1/two_sin" => WorkspaceArray{Vector{Float64}}("simulation1/two_sin")

)

1.2. Обработка переменной simout

В полученной переменной simout, в словаре dictхранятся пары ключ (имя сигнала) => значение (сигнал - время и значения/вектор значений):

In [ ]:
# получаем все имена сигналов из simout
dict_keys = simout.dict |> keys |> collect 
Out[0]:
4-element Vector{String}:
 "simulation1/sin_5Hz"
 "simulation1/sin_2Hz"
 "simulation1/Генератор синусоиды.1"
 "simulation1/two_sin"

Значения для первого ключа - simulation/sin_5Hz будут выглядеть так:

In [ ]:
collect(simout.dict["simulation1/sin_5Hz"])
Out[0]:
101×2 DataFrame
76 rows omitted
Rowtimevalue
Float64Float64
10.00.0
20.010.309017
30.020.587785
40.030.809017
50.040.951057
60.051.0
70.060.951057
80.070.809017
90.080.587785
100.090.309017
110.11.22465e-16
120.11-0.309017
130.12-0.587785
900.890.309017
910.91.10218e-15
920.91-0.309017
930.92-0.587785
940.93-0.809017
950.94-0.951057
960.95-1.0
970.96-0.951057
980.97-0.809017
990.98-0.587785
1000.99-0.309017
1011.0-1.22465e-15

То, есть, в значении словаря dict для имени нашего сигнала simulation/sin_5Hz хранится объект WorkspaceArray с полями time и value. Обращаясь к этим полям, можно получить отметки времени сигнала и сами значения и построить график:

In [ ]:
t1 = simout.dict["simulation1/sin_5Hz"].time
val1 = simout.dict["simulation1/sin_5Hz"].value
gr(format=:png)
plot(t1, val1; label="simulation1/sin_5Hz")
Out[0]:
No description has been provided for this image

1.3. Векторный сигнал в value

Сигнал, который мы взяли для построения выше был скалярным - в один момент времени записывалось одно значение типа Float64.
Рассмотрим иной случай, когда сигнал simulation/two_sin - вектор значений Float64:

In [ ]:
simout.dict["simulation1/two_sin"].value[1] # получаем первый элемент из массива значений сигнала simulation/two_sin
Out[0]:
2-element Vector{Float64}:
 0.0
 0.0

Это, как видно, вектор из двух значений. О том же самом нам сообщит и поле dimension словаря dict

In [ ]:
simout.dict["simulation1/two_sin"].dimension # получаем значение, хранящееся в поле dimension
Out[0]:
(2,)

Отсюда следует: размерность значений поля value - 2×1. И построить их таким же способом, как раньше, не получится:

In [ ]:
t2 = simout.dict["simulation1/two_sin"].time
val2 = simout.dict["simulation1/two_sin"].value

plot(t2, val2; label="simulation1/two_sin")
Out[0]:
print device already activated
print device already activated

Чтобы получить индексированные массивы отдельных сигналов, и построить их, используем getindex():

In [ ]:
val2_1 = getindex.(val2, 1)
val2_2 = getindex.(val2, 2)

plot(t2, val2_1; label="simulation1/two_sin[1]")
plot!(t2, val2_2; label="simulation1/two_sin[2]")
Out[0]:
No description has been provided for this image
print device already activated

1.4. Неименованный сигнал

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

In [ ]:
collect(simout.dict["simulation1/Генератор синусоиды.1"])
Out[0]:
101×2 DataFrame
76 rows omitted
Rowtimevalue
Float64Float64
10.00.0
20.010.425779
30.020.770513
40.030.968583
50.040.982287
60.050.809017
70.060.481754
80.070.0627905
90.08-0.368125
100.09-0.728969
110.1-0.951057
120.11-0.992115
130.12-0.844328
900.890.992115
910.90.951057
920.910.728969
930.920.368125
940.93-0.0627905
950.94-0.481754
960.95-0.809017
970.96-0.982287
980.97-0.968583
990.98-0.770513
1000.99-0.425779
1011.0-1.71451e-15

Рекомендация

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

1.5. Прореживание

Размерность массивов, которые мы передаём в plot() для построения графика, составляет всего лишь:

In [ ]:
simout.dict["simulation1/sin_5Hz"].value |> size # определяем размерность вектора значений сигнала simulation1/sin_5Hz
Out[0]:
(101,)

101 элемент в векторе, который в gr() бэкенде Plots, да ещё и с форматом изображения format = :png выводится на график достаточно быстро.

Предостережение

Например, если попытаться построить изображение svg в интерактивном бэкенде plotlyjs() для вектора из 100 000 элементов, придётся использовать значительно больше ресурсов и времени.

В случае, если для визуализации важна не каждая точка, можно использовать самый простой вариант - ленивые срезы WorkspaceArray:

In [ ]:
simout.dict["simulation1/sin_5Hz"].value[1:2:end]  |> size # получаем вектор значений сигнала с шагом 2 и определяем новую размерность
Out[0]:
(51,)

Ленивые срезы массива позволяют использовать для построения не все значения, а с шагом 2 ([1:2:end]) или иным ([1:3:end], [1:4:end], [1:5:end] и т.д.).

Ленивый срез поможет также выделить из сигнала начало ([1:20]) или конец ([40:end]).

Предостережение

Такой подход удобен для прореживания постоянных, слабоизменяющихся, а также гармонических сигналов. При прореживании последних необходимо использовать теорему Котельникова - максимальная частота в сигнале после прореживания должна быть меньше половины новой частоты дискретизации для выполнения условия Найквиста.

Рекомендация

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

2. Используем блоки To Workspace

2.1. Получение переменных

Чтобы необходимые данные из модели записались в рабочую область, нужно сделать следующее:

  1. На каждый записываемый сигнал (массив) установить блок ToWorkspace (В рабочую область).
  2. Дать имена записываемых в блоках переменных

Критично

На втором шаге нужно быть предельно внимательными - имя переменной в блоке должно быть задано с учетом правил именования переменных Julia.

image.png

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

In [ ]:
model2_path = "$(@__DIR__)/simulation2.engee" 
engee.load(model2_path) |> engee.run; # загрузили и выполнили модель
engee.close(engee.gcm(), force=true) # закрыли модель

sin_2Hz, sin_5Hz, two_sin, Генератор_синусоиды
Out[0]:
(WorkspaceArray{Float64}("sin_2Hz"), WorkspaceArray{Float64}("sin_5Hz"), WorkspaceArray{Vector{Float64}}("two_sin"), WorkspaceArray{Float64}("Генератор_синусоиды"))

2.2. Обработка переменных

Эти переменные также имеют типы WorspaceArray, и к ним применимы те же способы обработки, которые мы использовали в разделах 1.2 и 1.3.

2.3 Прореживание

Помимо программных способов прореживания, которые были упомянуты в разделе 1.5, удобно также предварительно использовать стандартные для блоков способы понижения частоты дискретизации:

  • путём снижения частоты дискретизации в блоке ToWorkspace,
  • при помощи блока Rate Transition (Переход между частотами дискретизации)

Снижение частоты дискретизации (Sample time: -1 -> 0.1)

image.png

Переход между частотами дискретизации:

image.png

Заключение

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

Определяющими факторами здесь являются потребности, компетенции и удобство пользователя.