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

Модель масса-пружина-демпфер

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

In [ ]:
Pkg.add(["Interpolations"])
In [ ]:
gr()

Описание моделей

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

image.png

Мы рассмотрим направленную модель msd_causal.engee, которая осуществляет численное интегрирование уравнения $m \ddot x + k x + b \dot x = 0$.

image.png

Можно наглядно убедиться, что если выразить из этой формулы уравнение для ускорения груза, то именно оно и приходит на вход интегратора $\frac{1}{s^2}$.

$$\ddot x = \frac{-k x - b \dot x}{m}$$

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

image.png

В нижней части модели находится группа элементов, напоминающая стандартные учебные иллюстрации – масса $m$ соединена с опорой при помощи пружины с жесткостью $k$ и демпфера с коэффициентом поглощения $b$ (изменяемые параметры прописаны в свойствах этих блоков).

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

Задание параметров моделей

Обе модели используют идентичные переменные параметры: масса груза m, жесткость пружины k и коэффициент поглощения демпфера b.

In [ ]:
b,k,m = 1,2,3;

Когда нужные переменные созданы, можно будет управлять моделями, меняя значения переменных в Окне переменных. При значениях b=1, k=2, m=3 мы должны наблюдать довольно медленный, инерционный процесс с заметным затуханием.

Загрузка и изучение моделей

При помощи однострочной команды мы либо загрузим (load), либо откроем уже загруженную модель (open). Здесь мы пользуемся синтаксисом однострочных условных операторов x = условие ? если_истинно : если_ложно.

In [ ]:
# Однострочная команда для открытия/загрузки модели
mc = "msd_causal" in [m.name for m in engee.get_all_models()] ? engee.open( "msd_causal" ) : engee.load( "$(@__DIR__)/msd_causal.engee" );
ma = "msd_acausal" in [m.name for m in engee.get_all_models()] ? engee.open( "msd_acausal" ) : engee.load( "$(@__DIR__)/msd_acausal.engee" );

Посмотрим, какие параметры интегратора прописаны в обеих моделях.

In [ ]:
using DelimitedFiles, DataFrames

# Собрем параметры моделей
mc_dict = engee.get_param( "msd_causal" );
ma_dict = engee.get_param( "msd_acausal" );
df_causal = DataFrame("Свойство"=>collect(keys(mc_dict)), "Направленная модель"=>collect(values(mc_dict)))
df_acausal = DataFrame("Свойство"=>collect(keys(ma_dict)), "Физическая модель"=>collect(values(ma_dict)))

# Таблица для сравнения параметров моделей
df = outerjoin(df_causal, df_acausal, on="Свойство")

# Отсеим свойства модели, которые нас интересуют
params_to_display = ["StartTime", "StopTime", "SolverType", "SolverName", "FixedStep"]
df[in(params_to_display).(df."Свойство"), :]
Out[0]:
5×3 DataFrame
RowСвойствоНаправленная модельФизическая модель
StringAnyAny
1SolverNameEulerEuler
2StopTime1010
3StartTime0.00.0
4SolverTypefixed-stepfixed-step
5FixedStep0.010.01

Мы видим, что обе модели имеют одинаковые настройки интегратора.

На самом деле, алгоритм численного решения физической модели задан в блоке Solver Configuration, но верхнеуровневый интегратор модели, который принимает сигналы от физической подсистемы и возвращает измерения в виде векторов, выполняет их дискретизацию с постоянным шагом 0.01 с.

Выполнение моделей с переменным шагом

Выполним модели и сохраним полученные результаты в переменные rc и ra для направленной и физической моделей соответственно.

In [ ]:
rc = engee.run( "msd_causal" );
ra = engee.run( "msd_acausal" );

Сравнение результатов выполнения:

In [ ]:
plot( rc["p"].time, rc["p"].value, title="Положение" )
p_pos = plot!( ra["p"].time, ra["p"].value )

plot( rc["v"].time, rc["v"].value, title="Скорость" )
p_vel = plot!( ra["v"].time, ra["v"].value )

plot( p_pos, p_vel, size=(600,200) )
Out[0]:

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

In [ ]:
using LaTeXStrings
d_pos = plot( ra["p"].value - rc["p"].value, title=L"\varepsilon_{положение}", legend=false)
d_vel = plot( ra["v"].value - rc["v"].value, title=L"\varepsilon_{скорость}", legend=false )

plot( d_pos, d_vel, size=(600,200) )
Out[0]:

Разница между результатами, очевидно, очень небольшая, порядок значения $10^{-3}$.

Посмотрим, что будет, если переключить "алгоритмическую" модель в тот же режим, в котором работает решатель физической модели.

Переключимся на решатель с постоянным шагом

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

In [ ]:
# Настроим направленную модель так, чтобы она решалась с переменным шагом интегрирования
engee.set_param!( "msd_causal", "SolverType"=>"variable-step" )
engee.set_param!( "msd_acausal", "SolverType"=>"variable-step" )

# Запустим модели
rc = engee.run( "msd_causal" );
ra = engee.run( "msd_acausal" );

Чтобы сравнить графики с разным разрешением нам потребуется интерполяция.

In [ ]:
using Interpolations
# Создаем два объекта для интерполяции положения и скорости каузальной модели
rc_p_interp_linear = linear_interpolation(rc["p"].time, rc["p"].value)
rc_v_interp_linear = linear_interpolation(rc["v"].time, rc["v"].value)

# Вычисляем разницу между интерполяцией и расчетом физической системы
e_pos = plot( ra["p"].time, ra["p"].value - rc_p_interp_linear.(ra["p"].time), title=L"\varepsilon_{положение}", legend=false )
e_vel = plot( ra["v"].time, ra["v"].value - rc_v_interp_linear.(ra["v"].time), title=L"\varepsilon_{скорость}", legend=false )

plot( e_pos, e_vel, size=(600,200) )
Out[0]:

На этот раз, разница между физической моделью и направленной уже гораздо меньше - расхождение имеет порядок значений $10^{-8}$.

В Engee встроен Инспектор данных, который позволит сравнить разные результаты запусков между собой.

image.png

Закрываем модели без сохранения

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

In [ ]:
engee.close( "msd_causal", force=true );
engee.close( "msd_acausal", force=true );

Заключение

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

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