В этой части посмотрим, как при помощи управления с обратной связью выйти на орбиту с требуемой ориентацией в пространстве. Приложений для такой задачи может быть множество - это и выполнение контрактов по выводу спутников, и спасение незадачливых кербалов, и запуск орбитальных сканеров, и выход на опорные орбиты для запуска к другим планетам и спутникам (особенно если стартовать не с Кербина). Конечно, в принципе можно сначала выйти на орбиту как попало, а потом выходить в нужную плоскость орбитальным маневрированием. Но это весьма затратно, поэтому целесообразно по возможности выходить в требуемую плоскость прямо со старта.
Плоскость орбиты, как мы помним, задаётся наклонением и долготой восходящего узла. Опытные игроки знают, что с земли можно выйти на орбиту, наклонение которой не меньше, чем широта места старта. Для пуска на наклонные орбиты нужно со старта держать курс не ровно на восток, а чуть севернее или южнее. Выход на нужную долготу восходящего узла осуществляется правильным подбором времени старта - по сути, нужно стартовать чуть раньше, чем плоскость целевой орбиты пройдёт над космодромом (точнее, космодром за счёт суточного вращения планеты попадёт в нужную плоскость).
Попадание в нужную долготу восходящего узла определяется выбором времени старта, поэтому с точки зрения управления ракетой на взлёте, основной является задача выхода на заданное наклонение. Ей и займёмся.
Теория старта на нужное наклонение описана, например,
тут. Сделаю здесь краткую выжимку.
1) Плоскость орбиты пересекает сферу планеты по окружности с центром в центре планеты.
2) Если меридиан пересекает орбиту с наклонением
i на широте
φ, то
путевой угол ξ между меридианом и орбитальной скоростью (азимут вектора орбитальной скорости на этой широте) определяется соотношением
(что демонстрируется методами линейной алгебры, но здесь вывод приводить не буду)
Из этой формулы как раз следует, что должно выполняться условие
|cos i| ≤ |cos φ|, т.е. минимальное наклонение равно широте места старта по модулю, а максимальное 180° минус широта места старта.
В стоковой игре космодром можно считать находящимся на экваторе, поэтому доступны, по сути, любые наклонения, а
sin ξ = cos i, т.е.
ξ = 90° - i.
3)Ракета, стоящая на поверхности, имеет орбитальную скорость, направленную на восток, и равную по модулю
где
Rbody - радиус тела, с которого ракета стартует,
Trot - длительность звёздных суток на этом теле. Для оценки азимута, под которым нужно пускать ракету на круговую орбиту с заданным наклонением, можно представить выход на орбиту как мгновенный подъём аппарата на высоту орбиты и добавку к скорости поверхности некоторого вектора до орбитальной скорости. Векторная диаграмма:
Рисунок 1. К расчёту азимута пуска. Из диаграммы ясно, как вычислить азимут пуска
Az:
Вывод:В идеальном случае (если планета не вращается) потребовалось бы держать курс всё время по азимуту
ξ(i, φ), в реальности нужно делать компенсацию вращения планеты. Поэтому со старта разворачиваем ракету по азимуту
Az, а потом по ходу дополнительно подтягиваем путевой угол к азимуту
ξ(i, φ) управлением по курсу. Осуществить это подруливание можно при помощи ПИД-регулятора, где в качестве ошибки брать разность истинного курса в данный момент и
ξ(i, φ).
Альтернативно можно разложить скорость на две компоненты: лежащую в плоскости с заданным наклонением и "боковую", т.е. ортогональную этой плоскости. В этом случае боковая компонента орбитальной скорости и будет ошибкой управления, которую требуется обнулить. Это также можно сделать ПИД-регулятором.
Мне больше нравится второй способ, поэтому привожу его реализацию.
Управление ракетой по тангажу производится тем же способом, что представлен в
первой части. Добавляется управление по курсу, которое делается ПИ-регулятором.
Дополнительная хитрость: после того, как "боковая" скорость зануляется, необходимое наклонение орбиты достигнуто. Это значит, что дальше нужно просто держать курс по азимуту
ξ(i, φ). Но в регуляторе накопится ошибка в интегральной компоненте, поскольку со старта до этого момента боковая скорость всегда была на восток, и регулятор будет продолжать тянуть скорость дальше на запад. Чтобы этого не происходило, при попадании на правильное наклонение обнулим интегральную компоненту регулятора и дальше движемся по курсу
ξ(i, φ).
function HDGctrl {
//Управление по рысканию
parameter ti.
parameter hdgpid.
local vh to vxcl(up:vector,velocity:orbit). //горизонтальная орбитальная скорость
local svh to vxcl(up:vector,velocity:surface). // горизонтальная скорость отн. поверхности
// если требуемая плоскость ближе к экватору, чем текущая широта,
// считаем, что нужно лететь прямо на восток или на запад
local xi to arcsin( max(-1, min( 1, cos(ti) / cos(latitude) ) ) ).
// проверка, аппарат движется на север или на юг
// если на юг, то курс идёт под тупым углом к меридиану
if svh:mag > 50 and vh:y < 0 { set xi to 180 - xi. }
local thdg to heading( xi, 0 ).
local vside to vdot(vxcl(thdg:vector,vh),thdg:rightvector). // боковая скорость
local hdg to xi + hdgpid:update(time:seconds, vside).
if hdgpid:pterm*hdgpid:iterm < 0 hdgpid:reset. // обнуление интегральной компоненты регулятора, когда пришли в нужную плоскость
local track to arctan2(vdot(vcrs(north:vector,vh:normalized), up:vector), vdot(north:vector,vh:normalized) ).
print "Track: " + round(track) + " " at (0,14).
print "Wanted track: " + round(xi) + " " at (20,14).
print "Target heading: " + round(hdg) + " " at (0,15).
return hdg.
}
function hdgpid_init {
// инициализация ПИД-регулятора для контроля рыскания
parameter ti, Horb.
// Расчёт азимута пуска
local vorb to sqrt(body:mu/Horb).
local vsurf to velocity:orbit:mag.
local xi to arcsin( max(-1, min( 1, cos(ti) / cos(latitude) ) ) ).
local azl to arcsin( (vorb*sin(xi) - vsurf) / sqrt( vsurf^2 + vorb^2 - 2*vsurf*vorb*sin(xi) ) ).
print "Launch azimuth: " + round(azl).
local Kp to 1/vorb.
if cos(azl) <> 0 {
set Kp to (xi - azl) / (vsurf*cos(xi)). // это значение даст курс в точности azl сразу после старта
}
local Ki to Kp/35.
return pidloop(Kp,Ki,0).
}
В качестве примера рассмотрим вывод простенького сканера на полярную орбиту (наклонение 90°).
Рисунок 1.1. Простенький сканер для отправки на полярную орбиту.
Программа носителя для выхода на орбиту:
function LVprogram {
parameter Horb to body:atm:height + 10000.
parameter ti to 0. // желаемое наклонение орбиты
parameter GTstart to 800.
parameter GTendAP to 55000.
parameter GTv45 to 500.
local maxTWR to 3.0.
local hdgpid to hdgpid_init(ti, Horb).
set hdgpid:setpoint to 0.
if LVmode = 0 {
// инициализация управления
lock throttle to 1.
local initialpos to ship:facing.
lock steering to initialpos.
startnextstage().
nextlvmode().
}
if LVmode = 1 {
// полёт до построения нужной высоты апоцентра
local vsm to velocity:surface:mag.
local GTStartSpd to vsm.
local Apo45 to apoapsis.
until apoapsis >= Horb {
set vsm to velocity:surface:mag.
if vsm <= GTv45 { set Apo45 to apoapsis. }
if alt:radar <= GTstart { set GTStartSpd to vsm. }
lock steering to heading( hdgctrl(ti, hdgpid), pitchctrl(GTStartSpd,GTstart,Apo45,GTendAP,GTv45) ). // основная команда управления
startnextstage().
CapTWR(maxTWR).
printtlm().
pidtlm(hdgpid).
wait 0.
}
nextlvmode().
}
if lvmode = 2 {
// поддержание апоцентра на желаемой высоте до выхода из атмосферы
lock throttle to 0.
lock steering to prograde.
until altitude > body:atm:height {
APkeep(Horb).
wait 0.
}
nextlvmode().
}
if lvmode = 3 {
lock throttle to 0.
print "We are in space. Deploying payload fairing. ".
wait 5.
stage.
nextlvmode().
}
if lvmode = 4 {
print "Deploying antenna.".
wait 3.
set an to ship:partsnamed("longAntenna")[0].
an:getmodule("ModuleRTAntenna"):doevent("activate").
nextlvmode().
}
if lvmode = 5 {
// подъём апоцентра до 20 км; не будем оставлять
// отработанные ступени орбитальным мусором,
// довыведение организуем силами спутника
aponode(20000).
nextlvmode().
}
if lvmode = 6 {
exenode().
nextlvmode().
}
if lvmode = 7 {
print "Releasing payload.".
lock steering to prograde.
wait 5.
stage.
}
}
Недостающие функции были частично
описаны в предыдущих частях гайда. Привожу те, которые касаются атмосферного полёта (остальные есть в приложенном файле).
function pidtlm {
// выводит на терминал состояние ПИД-регулятора
parameter pid.
print "Error: " + round(pid:error,2) + " "at (0,16).
print "PTerm: " + round(pid:pterm,2) + " " at (0,17).
print "ITerm: " + round(pid:iterm,2) + " " at (0,18).
print "DTerm: " + round(pid:dterm,2) + " " at (0,19).
print "Output: " + round(pid:output,2) + " " at (0,20).
}
function printtlm {
// базовая полётная телеметрия
local pitch to 90 - vang( up:vector, velocity:surface ).
print "Apoapsis: " + round( apoapsis/1000, 2 ) + " km " at (0,30).
print "Periapsis: " + round( periapsis/1000, 2 ) + " km " at (0,31).
print " Altitude: " + round( altitude/1000, 2 ) + " km " at (24,30).
print " Pitch: " + round( pitch ) + " deg " at (24,31).
}
function PitchCtrl {
// управление тангажом
parameter vstart. // скорость при начале разворота
parameter h0. // высота начала разворота
parameter AP45 is apoapsis. // апоцентр при угле тангажа 45°
parameter APstop is 60000. // по достижении этого апоцентра переход к горизонтальному полёту
parameter v45 is 500. // скорость, при которой тангаж должен быть 45°
if alt:radar < h0 {return 90.}
local vsm to velocity:surface:mag.
local pitch to 0.
if ( vsm < v45 ) {
set pitch to 90 - arctan( (vsm - vstart)/(v45 - vstart) ).
}
else {
set pitch to max(0, 45*(apoapsis - APstop) / (AP45 - APstop) ).
}
return pitch.
}
function CapTWR {
// ограничитель ускорения
parameter maxTWR is 3.0.
local g0 to 9.80665.
lock throttle to min(1, ship:mass*g0*maxTWR / max( ship:availablethrust, 0.001 ) ).
}
function startnextstage {
until ship:availablethrust > 0 {
if altitude<body:atm:height lock steering to srfprograde.
wait 0.5.
stage.
}
}
function APkeep {
// поддержание постоянной высоты апоцентра, пока не вышли из атмосферы
parameter apw.
local Kp to 200.
if apoapsis > apw { lock throttle to 0. }
else { lock throttle to max( 0.05, Kp*(apw - apoapsis)/apw ). }
printtlm().
}
function nextLVmode {
parameter newmode is LVmode+1.
set LVmode to newmode.
log "set LVmode to " + newmode + "." to "mode.ks".
}
Наконец, программа запуска спутника и ориентации на орбите в рабочее положение выглядит совсем просто:
function AlignToSun {
lock steering to lookdirup(-Sun:position,Kerbin:position).
}
function nextmode {
parameter newmode is satmode+1.
set satmode to newmode.
log "set satmode to " + newmode + "." to "mode.ks".
AlignToSun().
}
function satprogram {
if satmode = 0 {
copypath("0:/scannerlaunch.ks","lv.ks").
runpath("lv.ks").
local Hsat to 80000.
LVprogram(Hsat,90,80,57500,575).
nextmode().
}
if satmode = 1 {
startnextstage().
panels on.
wait 5.
circularize().
nextmode().
}
until false AlignToSun().
}
local satmode to 0.
local Hsat to altitude.
local LVmode to 0.
if exists("mode.ks") { runpath("mode.ks"). }
else nextmode(0).
satprogram().
(функция circularize описана
ранее).
Работа скрипта приводит к следующей рабочей орбите:
Рисунок 1.2. Сканер на рабочей орбите.
Замечу, что приведённые скрипты работают вне зависимости от того, с какой широты будет запускаться спутник - если целевое наклонение орбиты будет доступно, то спутник будет выведен.
Для выхода в нужную орбитальную плоскость, т.е. на требуемое наклонение с требуемой долготой восходящего узла, нужно стартовать в тот момент, когда космодром проходит под орбитальной плоскостью. Чтобы определить эту плоскость, вспомним, что долгота восходящего узла отсчитывается от вектора, называемого в kOS
SOLARPRIMEVECTOR. Чтобы получить нормаль к плоскости орбиты, нужно повернуть вектор полярной оси планеты (т.е.
V(0,1,0) в kOS), на наклонение орбиты вокруг вектора восходящего узла. А чтобы получить вектор восходящего узла, нужно повернуть
SOLARPRIMEVECTOR на долготу восходящего узла против часовой стрелки. Получается такая функция:
function DirANNorm {
// forevector - линия узлов, upvector - нормаль
parameter lan, incl.
local basedir to lookdirup(SolarPrimeVector, V(0,1,0)).
// углы поворота будут со знаком минус,
// т.к. в игре положительное направление вращения по часовой стрелке
set basedir to angleaxis(-lan, V(0,1,0))*basedir.
set basedir to angleaxis(-incl, basedir:forevector)*basedir.
return basedir.
}
"Точка старта проходит под плоскостью орбиты" означает, что угол между вектором от центра планеты к точке старта равен 90° (в реальности так сложно попасть, поэтому пусть будет "близок к 90°").
Этот способ позволяет понять, можно уже стартовать или надо подождать. Есть и другой способ, с помощью которого можно определить, сколько именно осталось ждать.
Определим "виртуальную" плоскость орбиты, которая получится, если из данной точки мгновенно попасть на нужное наклонение. Для этого нужно, чтобы орбитальная скорость была направлена по уже определённому азимуту
ξ(i, φ). Это направление легко получить из kOS как
Vvirt =
heading(xi, 0):vector. После чего находим нормаль к этой виртуальной орбите как
nvirt = [
Rship ×
Vvirt] и линию узлов как
ANvirt = [
V(0,1,0) ×
nvirt]. Угол между
ANvirt и
SOLARPRIMEVECTOR - это долгота восходящего узла, если стартовать прямо сейчас. Время, которое нужно подождать, равно, очевидно,
Δt = (LANdesired - LANvirt) Trot / (2π).
Желательно, конечно, стартовать не прямо в это время, а чуть пораньше (в стоке - примерно на минуту, в реальности с Земли примерно за 5 минут), т.к. поворот орбитальной скорости в нужную плоскость происходит не мгновенно.
Кроме старта в правильно подобранное время, неплохо бы придумать алгоритм, который бы постепенно приводил ракету в заданную плоскость. В
этом видео предлагается рулить согласно уравнению пересечения плоскости со сферой в координатах "широта-долгота". Я предлагаю альтернативный метод: рулить на основе расстояния от нужной плоскости.
Расстояние от ракеты до произвольной плоскости, проходящей через центр планеты, равно
d = (Rship · n),
где
n - вектор нормали к плоскости. Это расстояние будет со знаком, и однозначно определяет, в какую сторону нужно рулить:
при
d > 0 нужно брать азимут больше
ξ(i, φ);
при
d < 0 нужно брать азимут меньше
ξ(i, φ).
Для такого подруливания опять будем использовать ПИД-регулятор.
Ожидание окна для запуска:
function waitwindow {
parameter ti, tlan, leadtime.
local xi to arcsin( max(-1, min( 1, cos(ti) / cos(latitude) ) ) ).
local Vvirt to heading(xi, 0):vector.
local nvirt to vcrs(body:position, Vvirt).
local ANvirt to vcrs(nvirt, V(0,1,0)).
local LANvirt to arctan2( vdot( V(0,1,0), vcrs(ANvirt, solarprimevector) ), vdot(ANvirt, solarprimevector) ).
local landiff to tlan - LANvirt.
until landiff > 0 { set landiff to landiff + 360. }.
warpfor(landiff/360 * body:rotationperiod - leadtime).
}
Управление рысканием:
function HDGctrl {
//Управление по рысканию
parameter ti.
parameter tlan.
parameter hdgpid.
local vh to vxcl(up:vector,velocity:orbit). //горизонтальная орбитальная скорость
local svh to vxcl(up:vector,velocity:surface). // горизонтальная скорость отн. поверхности
// если требуемая плоскость ближе к экватору, чем текущая широта,
// считаем, что нужно лететь прямо на восток или на запад
local xi to arcsin( max(-1, min( 1, cos(ti) / cos(latitude) ) ) ).
// проверка, аппарат движется на север или на юг
// если на юг, то курс идёт под тупым углом к меридиану
if svh:mag > 50 and vh:y < 0 { set xi to 180 - xi. }
local thdg to heading( xi, 0 ).
local distkm to vdot(DirANNorm(tlan,ti):upvector,body:position)/1000.
local hdg to xi + hdgpid:update(time:seconds, distkm).
if hdgpid:pterm*hdgpid:iterm < 0 or hdgpid:pterm*hdgpid:iterm > hdgpid:pterm^2 { // когда пришли в нужную плоскость
hdgpid:reset. // или интегральное слагаемое слишком большое, обнулить интеграл
}
local track to arctan2(vdot(vcrs(north:vector,vh:normalized), up:vector), vdot(north:vector,vh:normalized) ).
print "Track: " + round(track) + " " at (0,14).
print "Wanted track: " + round(xi) + " " at (20,14).
print "Target heading: " + round(hdg) + " " at (0,15).
return hdg.
}
ПИД-регулятор для контроля рыскания:
function hdgpid_init {
parameter leadtime.
local Kd to 200.
local Kp to Kd^2*4e-5.
local Ki to Kp/(10*leadtime).
return pidloop(Kp,Ki,Kd).
}
Как написано в
предыдущей статье, при удержании координаты путём регулировки тяги достаточно ПД-регулирования. При этом координата подчиняется уравнению осциллятора с трением
d2x/dt2 = -2γdx/dt - ω02x
и наиболее быстрый выход к нулю происходит при
γ = ω0. Характерное время равно при этом
1/γ.
Для данной задачи
x - расстояние до желаемой плоскости орбиты, и при достаточно близком к горизонту тангажу можно считать, что
d2x/dt2 = αT/m,
где
α - угол в радианах между направлением рыскания ракеты и
ξ(i, φ),
T - проекция тяги на горизонтальную плоскость,
m - масса ракеты.
Поскольку угол
α контролируется ПИД-регулятором, то
α = 0,001·(-KP x - KD dx/dt) · π/180,
(множитель 0,001 соответствует переводу из метров в километры).
Сопоставляя уравнения, получаем
γ = 0,001·KD·(T/m)·(π/360)
ω02 = 0,001·KD·(T/m)·(π/180)
Подставляя типичное значение отношения тяги к массе для верхних ступеней (т.к. на нижней поворот по рысканию меньше влияет на плоскость орбиты из-за малого угла наклона к вертикали)
T/m ~ g = 9.8 и желаемое время выхода к равновесию 1 минута (
1/γ = 60), получаем
KD ~ 200.
Из условия
γ = ω0 получается
KP = 0,001·KD2·(T/m)·(π/720).
Коэффициент усиления для интегрального члена был выбран таким образом, чтобы не набирать слишком большое значение за время выхода в плоскость.
Программа носителя отличается от предыдущей только добавлением в число аргументов, помимо желаемого наклонения, ещё и желаемой долготы узла, и ожиданием окна для старта при помощи функции WAITWINDOW.
Для наиболее эффективной работы ПИД-регулятора нужно подобрать хорошее время старта. Для прилагаемого крафта наилучший результат оказывается, если стартовать за 75 секунд до поворота космодрома в орбитальную плоскость. Для других аппаратов это время может быть немного другим, в зависимости от длительности участка вывода.
[media=//www.youtube.com/watch?v=CNwk_AVX3IQ]
В следующей части рассмотрим использования ПИД-регуляторов при программировании посадки на безатмосферные тела.
Ракета оптимизирована под FAR, в стоке может не взлететь.
Требуются kOS и RemoteTech.
ships.zip