Моделирование Cartpole и реализация на Python: управление PID, LQR и MPC
Задача CartPole — удержать стержень вертикально (угол 0) и тележку в центре направляющей (положение 0).
В руководстве не приводится вывод формул. Если хотите разобраться в основах PID, LQR и MPC, изучите их отдельно.
I. Среда и предварительные знания
- Windows
- Матричная теория и теория управления
- Установка окружения — см.
requirements.txt
II. Моделирование Cartpole
Ниже приведён фрагмент официальной динамической модели Cartpole, см. http://incompleteideas.net/sutton/book/code/pole.c
Подробности — в файле cartpole_env.py из папки code. Во время практики, чтобы применить линейные регуляторы, мы сделали аппроксимацию динамической модели; её описание дано ниже.
# temp = (force + self.polemass_length * theta_dot * theta_dot * sintheta) / self.total_mass
# thetaacc = (self.gravity * sintheta - costheta * temp) / (self.length * (4.0 / 3.0 - self.masspole * costheta * costheta / self.total_mass))
# xacc = temp - self.polemass_length * thetaacc * costheta / self.total_mass
В виде формул это выглядит так:
Для линейного регулятора угол маятника мал, поэтому его можно аппроксимировать: , , . После такой аппроксимации уравнения принимают вид:
Запишем систему в пространстве состояний:
Пространство состояний используется в коде LQR и MPC. Соответствующий код на Python:
A = np.eye(4) + del_t * np.array([[0, 1, 0, 0],
[0, 0, (-3*m*g)/(m+4*M), 0],
[0, 0, 0, 1],
[0, 0, (3*(M+m)*g)/(L*(m+4*M)), 0]])
B = del_t * np.array([[0],
[(1/(M+m))+((3*m)/(m+4*M))],
[0],
[-(3)/(L*(m+4*M))]])
III. Разбор кода
1. Регулятор PID
В коде используется только PD-регулирование: отдельно вычисляются отклонения положения тележки и угла стержня, и по ним выполняется PD-регулирование. Ключевой код PD-регулятора:
def pid_cart(self, position):
bias = position
d_bias = bias - self.bias_cart_1
balance = self.kp_cart * bias + self.kd_cart * d_bias
self.bias_cart_1 = bias
return balance
def pid_pole(self, angle):
bias = angle
d_bias = bias - self.bias_pole_1
balance = -self.kp_pole * bias - self.kd_pole * d_bias
self.bias_pole_1 = bias
return balance
2. Регулятор LQR
Постановка задачи дискретного LQR:
Этапы вычисления дискретного LQR:
- Решить уравнение для матрицы :
- Вычислить матрицу обратной связи :
- Вычислить управляющее воздействие :
В практическом использовании LQR для Cartpole — весовая матрица перекрёстных членов состояния и управления. В задаче Cartpole перекрёстный член не нужно штрафовать, поэтому полагают нулевой матрицей.
Ключевой код LQR-регулятора:
def control_output(self, state_des, state_now):
S = np.matrix(linalg.solve_discrete_are(self.A, self.B, self.Q, self.R))
K = np.matrix(linalg.inv(self.B.T * S * self.B + self.R) * (self.B.T * S * self.A))
# print("state_now", state_now)
# print("state_des", state_des)
action_list = -10*np.dot(K, state_now - state_des)
action = action_list[0,0]
return action
Уравнение Риккати решается функцией solve_discrete_are из модуля linalg.
3. Регулятор MPC
Основные компоненты MPC:
- Прогнозная модель: по текущему состоянию и будущим управляющим воздействиям предсказывает траекторию состояния системы.
- Оптимизация на скользящем горизонте: ищет последовательность управляющих воздействий, при которой критерий качества (функция стоимости) оптимален на горизонте прогноза.
- Коррекция по обратной связи: корректирует прогноз модели по измерениям и на следующем шаге заново прогнозирует и оптимизирует по новому состоянию.
Постановка линейной задачи MPC:
Здесь:
- — горизонт прогноза (длина предсказания).
- — прогноз состояния в момент , сделанный в момент .
- — управляющее воздействие в момент , запланированное в момент .
- — весовая матрица состояния.
- — весовая матрица управляющего воздействия.
- — весовая матрица терминального состояния.
Ключевой код MPC-регулятора:
- Построение матриц задачи QP: вычисляются матрицы и из формулировки QP.
def get_QPMatrix(A, B, Q, R, F, N):
M = np.vstack([np.eye(n), np.zeros((N*n, n))])
C = np.zeros(((N+1)*n, N*p))
temp = np.eye(n)
for i in range(1,N+1):
rows = i * n + np.arange(n)
C[rows,:] = np.hstack([temp @ B, C[rows-n, :-p]])
temp = A @ temp
M[rows,:] = temp
Q_ = np.kron(np.eye(N), Q)
rows_Q, cols_Q = Q_.shape
rows_F, cols_F = F.shape
Q_bar = np.zeros((rows_Q+rows_F, cols_Q+cols_F))
Q_bar[:rows_Q, :cols_Q] = Q_
Q_bar[rows_Q:, cols_Q:] = F
R_bar = np.kron(np.eye(N), R)
# G = M.T @ Q_bar @ M
E = C.T @ Q_bar @ M
H = C.T @ Q_bar @ C + R_bar
return E, H
- Решение MPC в реальном времени:
CasADi — мощный фреймворк символьных вычислений и автоматического дифференцирования, который часто применяют для задач оптимального управления и нелинейной оптимизации.
def mpc_prediction(x_k, E, H, N, p):
# Определяем переменные оптимизации
U = ca.SX.sym('U', N * p)
# Определяем целевую функцию
objective = 0.5 * ca.mtimes([U.T, H, U]) + ca.mtimes([U.T, E, x_k])
qp = {'x': U, 'f': objective}
opts = {'print_time': False, 'ipopt': {'print_level': 0}}
solver = ca.nlpsol('solver', 'ipopt', qp, opts)
# Решаем задачу
sol = solver()
# Извлекаем оптимальное решение
U_k = sol['x'].full().flatten()
u_k = 10*U_k[:p] # берём первый элемент результата
return u_k
IV. Результаты кода
1. Результаты PID-управления
2. Результаты LQR-управления
3. Результаты MPC-управления