Файл Task_2/main.cpp короче, чем у задачи про баллистику: нет мишени, нет проверки попадания. Здесь используются два графика и пошаговое численное интегрирование вместо готовой формулы.
Порядок частей тот же, что и всегда: подключения, состояние, кнопка, расчётная функция, main.
Три строки с решёткой – это препроцессор, помощник компилятора, который работает с текстом программы до самой компиляции. Читаются они так: «если имя M_PI ещё не определено – определить его как такое-то число».
Зачем такая осторожность? Число $\pi$ есть в заголовке <cmath> под именем M_PI, но не по стандарту языка, а по старой традиции Unix. Компилятор Microsoft его без специальных настроек не даёт. Проверка #ifndef решает вопрос раз и навсегда: где есть – возьмём готовое, где нет – объявим сами.
В задаче про баллистику та же проблема решена проще – там просто написано const float PI = 3.14159265f;. Оба способа рабочие.
Состояние задачи
int widhtWindow = 850;
int hieghtWindow = 1200;
float t = 0.0f; // текущее время
float dt = 0.05f; // шаг по времени
float alpha = 0.0f; // угол отклонения маятника
float v = 1.f; // скорость груза по дуге
Две переменные – alpha и v – это всё состояние маятника. Из них считается и картинка, и фазовая траектория, и энергия.
Окно вертикальное, 850 на 1200: два квадратных графика удобнее ставить друг над другом. (Опечатки в именах widht и hieght живут в файле с самого начала. Компилятору всё равно, а как пример того, что имена стоит перечитывать, они даже полезны.)
Маятник всегда стартует из нижней точки ($\alpha = 0$), и единственное, что задаётся из пульта, – с какой скоростью его туда «толкнули». Амплитуда получается сама: чем сильнее толчок, тем выше заберётся.
Связь между толчком и амплитудой даёт закон сохранения энергии:
$$\frac{v_0^2}{2} = 1 - \cos\alpha_{max}$$
Отсюда при $v_0 = 1$ получаем $\cos\alpha_{max} = 0.5$, то есть амплитуду ровно 60°. Хорошая проверка: запустите с единицей и посмотрите, докуда доходит груз.
clear_plot_history стирает старую фазовую траекторию – иначе новый запуск рисовался бы поверх предыдущего. Иногда это как раз нужно (сравнить две траектории), и тогда строку убирают.
Расчётная функция: начало
void calculation_function() {
if (get_bool_param("Пауза")) return;
float l = get_float_param("Длина стержня l");
float g = get_float_param("Тяжесть g");
if (l <= 0.f) l = 1.f; // деление на ноль: длина нулевой не бывает
t += dt;
set_float_param("Время", t);
Первая строка – ранний выход: если стоит галочка «Пауза», функция заканчивается немедленно, и ни расчёт, ни перерисовка не происходят. Картинка при этом не пропадает: библиотека продолжает показывать то, что было нарисовано на последнем кадре.
Длина и тяжесть читаются из пульта каждый кадр – в отличие от начальной скорости, которая читается один раз по кнопке. Поэтому ручки l и g работают на лету: можно менять их прямо во время колебаний и смотреть, что происходит.
Проверка if (l <= 0.f) – защита от деления на ноль. Дальше будет v / l, и если выкрутить длину в ноль, результатом станет inf («бесконечность»), а следующим шагом – nan («не число»). Программа не упадёт, но всё состояние испортится, и вылечить это можно будет только перезапуском. Одна строка защиты экономит десять минут недоумения.
Расчётная функция: шаг метода
// Симплектический Эйлер, шаг первый: угол по старой скорости.
alpha += v / l * dt;
if (alpha > M_PI) alpha -= 2.0f * (float)M_PI;
if (alpha < -M_PI) alpha += 2.0f * (float)M_PI;
// координаты груза
float x_m = l * sin(alpha);
float y_m = -l * cos(alpha);
std::vector<float> mx = {0.0f, x_m};
std::vector<float> my = {0.0f, y_m};
clear_plot("Маятник");
add_plot_line("Маятник", mx, my, "Стержень", BLUE, 2.f);
add_plot_point("Маятник", mx[1], my[1], "Груз", RED, 6.f);
// Шаг второй: скорость по силе, посчитанной уже в новой точке.
v += -g * sin(alpha) * dt;
Здесь сердце программы, и стоит проследить порядок действий по шагам.
Угол увеличивается на $v\,dt/l$ – по старой скорости;
угол приводится к диапазону $[-\pi; \pi]$;
считаются координаты груза и рисуется маятник;
Скорость увеличивается на $-g\sin\alpha\,dt$, где alpha – уже новый.
Пункты 1 и 4 – это и есть симплектическая схема, разобранная в статье о численном методе. Рисование между ними ничего не меняет: оно не трогает ни угол, ни скорость.
Координаты груза. Ось $y$ направлена вверх, а груз висит внизу, поэтому у косинуса минус:
$$x = l\sin\alpha, \qquad y = -l\cos\alpha$$
При $\alpha = 0$ получается $(0, -l)$ – груз строго под точкой подвеса. Верно.
Стержень рисуется как линия из двух точек – от подвеса $(0, 0)$ до груза. Запись mx[1] берёт из вектора элемент с номером 1, то есть второй – нумерация в C++ идёт с нуля.
set_float_param пишет числа в показания пульта – поля, которые выглядят как ручки, но только для чтения. Их завели в main через add_output_float. Число в таком поле можно выделить и скопировать, а изменить нельзя: его пишет программа.
Фазовая диаграмма рисуется двумя вызовами, и это разные вещи:
add_plot_history_point добавляет точку в историю. История копится сама и рисуется вся целиком – так получается синяя кривая. Число 2000 в конце – сколько последних точек хранить; когда их станет больше, самые старые начнут отбрасываться, и хвост траектории будет постепенно исчезать;
add_plot_point рисует одну точку кадра – красный маркер текущего состояния. Он живёт до следующего clear_plot, поэтому его добавляют заново каждый кадр.
Именно поэтому clear_plot("Фазовая диаграмма") не стирает синюю линию: он убирает объекты кадра, а история – отдельная сущность и чистится своей функцией clear_plot_history.
Ручки заводятся с ограничителями: add_float_param("Длина стержня l", 1.0f, 0.1f, 5.0f, 0.1f) означает «начальное значение 1, минимум 0.1, максимум 5, шаг изменения 0.1». Минимум 0.1 – ещё одна защита от нулевой длины, теперь уже на уровне интерфейса.
Показания заводятся тремя строками add_output_float, и передаваемое значение – просто начальное, дальше их обновляет расчёт.
Границы фазовой диаграммы стоит разобрать отдельно. По углу взят диапазон от $-3.3$ до $3.3$ – чуть шире, чем $\pm\pi \approx 3.14$, куда угол и попадает после нормализации. По скорости $\pm 3$.
Последний add_plot_history_point до запуска ставит начальную точку траектории: без него первая точка появилась бы только после первого шага.