Skip to content

Commit

Permalink
Major update with a number of changes:
Browse files Browse the repository at this point in the history
- For transfers, we now do a two stage burn. Initial and correction.
- Correction burn uses the new Maneuver Node functionality to optimize the encounter
- There is now a mission to fly to minmus (go_minmus) and it seems to work
- Updated debugging information
- More reliable warp code
  • Loading branch information
appenz committed Feb 5, 2024
1 parent 348d75f commit fd84a0f
Show file tree
Hide file tree
Showing 11 changed files with 431 additions and 61 deletions.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,32 @@ Right now there are the following missions:
- launch_to_orbit: launches a vessel into low Kerbin orbit.
- land_athmosphere: lands a vessel from orbit around Kerbin. Assumes you have parachutes. Tries to land near the KSC.
- go_mun : Launches a vessel to the Mun and lands in a flat area near the Mun's equator. Needs enough delta_v to get there.
- return_to_kerbin: returns to kerbin from a moon in the Kerbin system. Tested with the Mun. Assumes you have parachutes.
- go_minmus : Launches a vessel to Minmus and lands it... somewhere.
- return_to_kerbin: returns to kerbin from a moon in the Kerbin system. Tested with the Mun. Assumes you have parachutes

The scripts will stage as needed. Debug and status output is shown in the console, opening it is highly recommended.

## Phases of a Mission

Initially the vessel is launched, gravity turn is performed and we climb to close to the target apoapsis. Usually an additional burn is performed to fune tune once we have exited the atmosphere. After circularization, we check if the inclination needs to be change.

Then the transfer is planned. For anything but Mun, the transfer from Kerbin will happen in two phases:
1. An initial burn to get on an escape trajectory from Kerbin
2. A second burn executed above 100k km altitude to fine tune the trajectory and get a good encounter

After the correction burn, we wait until we are in the spehere of influence (SOI) of the target body. Once that is the case, we circularize, lower periapsis and lower apopasis to the target orbit height.

Last we pick a spot to land, slow down close to that spot and the auto lander plans and executed a (hopefully) soft landing. Right now landing spots are hardcoded, we do no check the terrain for suitability.

Return starts with an initial burn to an apoapsis that is circularized. We then claculate ejection angles (at least in the Kerbin system) and perform an ejection burn. This should put us into an elliptical orbit around Kerbin. We then wait for the Apoapsis, lower periapsis and attempt aero capture. This may happen over a number of passes to reduce heat.

Once Periapsis is below sea level, the landing code takes over, waits for safe opening of parachutes and lands.

Most scripts are written that they can be aborted and re-started at most time. Quicksave is your friend.

## Where to get help with the scripts

Best place to get help are the [KSP forums](https://forum.kerbalspaceprogram.com/topic/214543-release-kontrolsystem2-042/).

If you encounter a bug or have a suggestions, please file it here on GitHub.

20 changes: 15 additions & 5 deletions glib/display.to2
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,33 @@ pub sync fn warning( s : string) -> Unit = {
}

pub sync fn debug( s : string) -> Unit = {
//CONSOLE.print_line(" Debug: "+s)
CONSOLE.print_line(" > "+s)
}


// Pretty distance print
pub sync fn pretty_distance(d : float) -> string = {
pub sync fn pretty_distance(dd : float) -> string = {
let sign = ""
let d = dd
if (dd < 0) {
sign = "-"
d = -dd
}
if (d>1000000)
return(format("{0:N0} km", d/1000 ) )
return(format("{1}{0:N0} km", (d/1000, sign) ) )
if (d>1000)
return(format("{0:N2} km", d/1000 ) )
return(format("{0:N1} m", d ) )
return(format("{1}{0:N2} km", (d/1000, sign) ) )
return(format("{1}{0:N1} m", (d, sign) ) )
}


// Pretty print time
pub sync fn pretty_time(time : float) -> string = {
let s = time.to_int
if (s>21600)
return(format("{0:N0}d {1:N0}:{2,2:00}:{3,2:00}", (s/21600, (s/3600)%6, (s/60)%60, s%60) ))
if (s>3600)
return(format("{0:N0}:{1,2:00}:{2,2:00}", (s/3600, (s/60)%60, s%60) ))
if (s>200)
return(format("{0:N0}:{1,2:00}", (s/60, s%60) ) )
return time.to_int.to_string()+" s"
Expand Down
4 changes: 2 additions & 2 deletions glib/maneuver_vacuum.to2
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ impl Node {

// Returns an invalid node
pub sync fn ErrorNode() -> Node = {
return Node(-1, vec3(1,2,3))
return Node(-1, vec3(0,0,0))
}

//
Expand Down Expand Up @@ -176,7 +176,7 @@ pub sync fn change_incl(vessel: Vessel, target_orbit: Orbit) -> Node = {
const v_vessel = vessel.orbit.global_velocity(t_node).to_local(frame)
const dv_mag = -target_orbit.orbit_normal.normalized.dot(v_vessel)
const dv = dv_mag*vessel.orbit.orbit_normal
//warning(format("{0:N3} {1:N3} delta: {2:N3} mean_v: {3:N4} t: {4}", (ma_v, dn_ta, (dn_ta-ma_v), vessel.orbit.mean_motion, pretty_dt(t_node) )))
warning(format("{0:N3} {1:N3} delta: {2:N3} mean_v: {3:N4} t: {4}", (ma_v, dn_ta, (dn_ta-ma_v), vessel.orbit.mean_motion, pretty_dt(t_node) )))
return Node(t_node,dv)
}

Expand Down
44 changes: 41 additions & 3 deletions glib/mission.to2
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ impl Mission {
self.name
}

// Helper function to return Apoapsis of the vessel in a safe way
// Returns -1 if undefined.
sync fn apoapsis(self) -> float = {
if (self.vessel.orbit.apoapsis.defined) return self.vessel.orbit.apoapsis.value
return 0
}

// Check if all engines have flamed out.
sync fn flameout(self) -> bool = {
for(engine in self.vessel.engines)
Expand Down Expand Up @@ -172,6 +179,37 @@ impl Mission {
self.burnNextNode()
}

// Warp to a specific time. Don't return until we are there.
fn warpToTime(self, t: float) -> Unit = {
const con = self.console.value
warp_to(t)
while(current_time()<t) {
con.h5 = format("Warp Time left: {0}", pretty_dt(t))
con.update1()
if (ksp::game::warp::current_index() == 0) {
warp_to(t)
sleep(0.1)
}
}
con.h5 = ""
}

// Warp to a specific altitude. Surprisingly easy.
fn warpToAltitude(self, alt_target: int) -> Unit = {
const orbit = self.vessel.orbit
const con = self.console.value

con.log(format("Warping to altitude {0}", pretty_distance(alt_target)))
const now = current_time()
const eta = orbit.next_time_of_radius(now, alt_target+orbit.reference_body.radius)
if (eta.defined) {
self.warpToTime(eta.value)
con.log(format(" arrived at altitude {0} m", pretty_distance(self.vessel.altitude_sealevel)))
} else {
panic("Orbit won't reach altitude.")
}
}

// Warp to the SOI change. This is surprisingly hard.
fn warpToSOI(self, dst: string) -> Unit = {
const vessel = self.vessel
Expand All @@ -183,9 +221,9 @@ impl Mission {
let t = find_encounter(vessel, body_dst, body_dst.SOI_radius*0.9)
let d = find_distance_t(vessel, body_dst, t)

con.log(format(" encounter in {0} distance {1}", (pretty_distance(d),pretty_dt(t))))
con.log(format(" encounter in {1} distance {0}", (pretty_distance(d),pretty_dt(t))))

warp_to(t)
self.warpToTime(t)
con.h5 = ""

while(current_time()<t && vessel.orbit.reference_body.name != dst) {
Expand Down Expand Up @@ -214,7 +252,7 @@ impl Mission {

con.log(format(" SoI exit in {0} distance {1}", (pretty_distance(d),pretty_dt(t))))

warp_to(t)
self.warpToTime(t)
con.h5 = ""

while(current_time()<t && vessel.orbit.reference_body.name == body_src.name) {
Expand Down
126 changes: 109 additions & 17 deletions glib/transfer.to2
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use { warp_to } from ksp::game::warp
use { acos_deg, clamp, sqrt, PI, min, max, acos } from core::math
use { format } from core::str

use { pretty_time, pretty_dt, warning, panic, debug } from glib::display
use { rad2deg, deg2rad, deg360 } from glib::utility
use { pretty_time, pretty_dt, pretty_distance, warning, panic, debug } from glib::display
use { rad2deg, deg2rad, deg360, find_encounter, find_distance_t, find_encounter_o, find_distance_o } from glib::utility
use { vv_alt, vv_axis, Node, ErrorNode } from glib::maneuver_vacuum
use { Mission } from glib::mission

Expand All @@ -30,7 +30,7 @@ pub struct TransferInfo(psrc: string, pdst: string, palt: int, pa_phase: float,
v_eject: float = pv_eject
}

pub struct Transfer() {
pub struct Transfer(mission: Mission, dst:string, target_alt: float) {
table: TransferInfo[] = [
TransferInfo("Kerbin", "Eve", 1000, -54.13, 143.44+180, 2226.01),
TransferInfo("Kerbin", "Duna", 1000, 44.36, 138.75, 2267.83),
Expand All @@ -41,8 +41,10 @@ pub struct Transfer() {
TransferInfo("Dres", "Kerbin", 500,-329.68, 90.77+180, 1589.12)
]

mission: Mission = mission
src : string = "source"
dst : string = "dst"
dst : string = dst
target_alt : float = target_alt
alt : float = 0
alt_new : float = 0
a_phase : float = 0
Expand Down Expand Up @@ -111,20 +113,18 @@ impl Transfer {
// - Planned transfer will be recorded in phase angle and ejection angle, velocity & time
// - Function will not check if this actually results in an encounter

fn planTransfer(self, mission: Mission, dst:string, target_alt: float)-> Unit = {
let con = mission.console.value
let vessel = mission.vessel
fn getTransferNode(self)-> Node = {
let con = self.mission.console.value
let vessel = self.mission.vessel
let orbit = vessel.orbit
self.src = orbit.reference_body.name
self.dst = dst

const t = self.table
if ( !find_body(dst).success ) {
if ( !find_body(self.dst).success ) {
panic(format("Transfer destination {1} not found", (self.dst)))
return
return ErrorNode()
}
const body_dst = find_body(self.dst).value
con.log(format("Planning Transfer: {0} -> {1}",(self.src, target_alt)))

// Check if we transfer planet->moon or star->planet (vs. planet to planet)

Expand All @@ -146,7 +146,7 @@ impl Transfer {
// Estimate altitude we need to burn to.
// Basic formula is: radiums of orbit of target - radius of current body - radius of target body - target altitude
// TODO: Need to adjust this for non-circular orbits by estimating rendevous time better. For now we just take 1/4 rotation.
self.alt_new = body_dst.orbit.radius(self.t_eject+body_dst.orbit.period/4)-orbit.reference_body.radius-body_dst.radius-target_alt
self.alt_new = body_dst.orbit.radius(self.t_eject+body_dst.orbit.period/4)-orbit.reference_body.radius-body_dst.radius-self.target_alt
// Calculate dV needed to the new altitude
self.v_eject = self.transferDeltaV(orbit, self.alt_new, self.t_eject)

Expand All @@ -160,7 +160,7 @@ impl Transfer {
//self.v_eject = t[i].v_eject
}
panic("Planet to Planet not implemented yet.")
return
return ErrorNode()
}
panic(format("No data found for transfer {0} -> {1}", (self.src, self.dst)))
}
Expand All @@ -170,14 +170,106 @@ impl Transfer {
( self.alt_new, pretty_dt(self.t_eject)) ))
con.log(format(" eject at phase angle {0:N1} ejection angle {1:N1} dV: {2:N} m/s",
(self.a_phase, self.a_eject, self.v_eject.magnitude)))

return Node(self.t_eject, self.v_eject)
}

fn getTransferNode(self)-> Node = {
if (self.t_eject == 0) {
panic("Can't add transfer node as none is planned.")
// Plan a Maneuver to fine-tune a transfer to a different body.
//
// The basic idea is that the initial transfer Node will get us close to a rendezvous, but
// an additional correction burn is required to get us in a good orbit.
// - This should be called after ejection burn has been executed
// - Main parameter is the target Periapsis at the target body

fn getCorrectionNode(self)-> Node = {
const con = self.mission.console.value
const vessel = self.mission.vessel
const dst_body = find_body(self.dst).value
const target_dist = self.target_alt+dst_body.radius

con.log(format("Planning correction burn to {0} alt {1}",(self.dst, self.target_alt)))

// Basic idea, do a gradient descent on a pro/retrograde burn to get the desired periapsis
// We use the game's maneuver nodes to do the calculation for us.

// Add a maneuver node to 2 minutes in the future
const maneuver_time = current_time()+120
vessel.maneuver.remove_all()
vessel.maneuver.add_burn_vector(maneuver_time, vec3(0,0,0))
yield()

// Get orbit after the maneuver. Run checks to make sure it worked.
if (vessel.maneuver.trajectory.length == 0) {
panic("Creating maneuver node failed.")
return ErrorNode()
}
return Node(self.t_eject, self.v_eject)

const new_orbit = vessel.maneuver.trajectory[0]
const node = vessel.maneuver.nodes[0]
sleep(0.5)

// Set up gradient descent
const speed = 0.05
let i = 0
let d_dv = 0.02
let d_best = 0.0
let t = 0.0

con.h3 = format("Planning correction burn for {0}",dst_body.name)
con.h5 = "No encounter yet."

// Estimate current distance
const t0 = find_encounter_o(new_orbit, dst_body.orbit)
let d_now = find_distance_o(new_orbit, dst_body.orbit, t0)
con.log(format(" pre-maneuver closest approach : {0} trying dV: {1} prograde",(pretty_distance(d_now), d_dv)))

// Optimize until we either have reached a minimum, or we are below the target altitude
while( (d_best > d_now && d_best > target_dist) || i < 10) {
i += 1
// Adjust burn and verify effect. We need to wait briefly after adjusting to let KSP catch up.
node.prograde = node.prograde+d_dv
sleep(0.1)

// Check if we are getting into the SOI of another body
if (vessel.maneuver.trajectory.length > 1) {
con.h5 = format(" Encounter with {0} detected.",(vessel.maneuver.trajectory[1].reference_body.name))
// This needs code to deal with this situation. For now, we just ignore.
}

// Calculate time and distance of closest approach
t = find_encounter_o(new_orbit, dst_body.orbit)
const d_next = find_distance_o(new_orbit, dst_body.orbit, t)

// Gradient descent
const d_remaining = d_next-dst_body.radius // Remaining distance to target altitude
const d_delta = (d_next-d_now)/d_dv // Change in distance from 1m/s prograde
const dv_est = -d_remaining/d_delta // Linear estimation of dV to reach target altitude
d_dv = clamp(dv_est*speed,-0.1,0.1) // Actual change per iteration is adjusted by speed
con.h4 = format("# {0} dV:{1:N3} distance old/new {2}/{3} (delta: {4})",
(i, node.prograde, pretty_distance(d_now), pretty_distance(d_next), pretty_distance(d_delta)) )
con.update_slow()

//con.log(format(" est. change for 1m/s dv: {0:N1} required/current dV: {1:N3}/{2:N3} (delta: {3:N3}) adjust {4:n3}",
// (pretty_distance(d_delta), dv_est+node.prograde, node.prograde, dv_est, d_dv)))
//con.log(format(" d_delta: {0:N3} = (d_next: {1:N3} - d_now: {2:N3})/d_dv: {3:N3}",
// (pretty_distance(d_delta), pretty_distance(d_next), pretty_distance(d_now), d_dv)))
//con.log(format(" dv_est: {0:N3} = d_remaining: {1:N3} / d_delta: {2:N3}",
// (dv_est, d_remaining, d_delta)))

d_best = d_now
d_now = d_next
}

con.log(format(" post-maneuver closest approach: {0} in {1} ({1} iterations)",(pretty_distance(d_now), pretty_dt(t), i)))

// Check how well we have done
if(vessel.maneuver.trajectory.length > 1) {
con.log(format(" encounter with {0}",(vessel.maneuver.trajectory[1].reference_body.name)))
}

const n_final = Node(maneuver_time, node.burn_vector)
vessel.maneuver.remove_all()
return n_final
}

}
Expand Down
Loading

0 comments on commit fd84a0f

Please sign in to comment.