///|
pub enum MissionEventKind {
OrbitInsertion
Maneuver
GroundContact
EclipseEntry
EclipseExit
PayloadOperation
Custom
} derive(Debug, Eq)
///|
pub fn mission_event_kind_name(kind : MissionEventKind) -> String {
match kind {
OrbitInsertion => "orbit-insertion"
Maneuver => "maneuver"
GroundContact => "ground-contact"
EclipseEntry => "eclipse-entry"
EclipseExit => "eclipse-exit"
PayloadOperation => "payload-operation"
Custom => "custom"
}
}
///|
pub fn all_mission_event_kinds() -> Array[MissionEventKind] {
[
OrbitInsertion,
Maneuver,
GroundContact,
EclipseEntry,
EclipseExit,
PayloadOperation,
Custom,
]
}
///|
pub struct MissionEvent {
time_s : Double
kind : MissionEventKind
label : String
delta_v_km_s : Double
duration_s : Double
} derive(Debug, Eq)
///|
pub struct DeltaVBudget {
planned_km_s : Double
reserve_fraction : Double
reserve_km_s : Double
total_km_s : Double
} derive(Debug, Eq)
///|
pub struct MissionTimeline {
events : Array[MissionEvent]
end_time_s : Double
} derive(Debug, Eq)
///|
pub struct PassWindow {
rise_time_s : Double
set_time_s : Double
max_elevation_rad : Double
samples : Int
} derive(Debug, Eq)
///|
pub fn MissionEvent::new(
time_s : Double,
kind : MissionEventKind,
label : String,
delta_v_km_s? : Double = 0.0,
duration_s? : Double = 0.0,
) -> MissionEvent {
{
time_s,
kind,
label,
delta_v_km_s: delta_v_km_s.max(0.0),
duration_s: duration_s.max(0.0),
}
}
///|
pub fn MissionTimeline::new(end_time_s? : Double = 0.0) -> MissionTimeline {
{ events: [], end_time_s: end_time_s.max(0.0) }
}
///|
pub fn MissionTimeline::add(
timeline : MissionTimeline,
event : MissionEvent,
) -> MissionTimeline {
let events = timeline.events.copy()
events.push(event)
{
events,
end_time_s: timeline.end_time_s.max(event.time_s + event.duration_s),
}
}
///|
pub fn MissionTimeline::sort_by_time(
timeline : MissionTimeline,
) -> MissionTimeline {
let result = timeline
for i in 0.. Double {
timeline.end_time_s
}
///|
pub fn MissionTimeline::total_delta_v(timeline : MissionTimeline) -> Double {
timeline.events.fold(init=0.0, (total, event) => total + event.delta_v_km_s)
}
///|
pub fn MissionTimeline::events_in_range(
timeline : MissionTimeline,
start_s : Double,
end_s : Double,
) -> Array[MissionEvent] {
timeline.events.filter(event => {
event.time_s >= start_s && event.time_s <= end_s
})
}
///|
pub fn make_delta_v_budget(
planned_km_s : Double,
reserve_fraction? : Double = 0.1,
) -> DeltaVBudget {
let planned = planned_km_s.max(0.0)
let reserve = reserve_fraction.max(0.0)
let reserve_value = planned * reserve
{
planned_km_s: planned,
reserve_fraction: reserve,
reserve_km_s: reserve_value,
total_km_s: planned + reserve_value,
}
}
///|
pub fn budget_remaining(budget : DeltaVBudget, spent_km_s : Double) -> Double {
budget.total_km_s - spent_km_s.max(0.0)
}
///|
pub fn budget_is_exceeded(budget : DeltaVBudget, spent_km_s : Double) -> Bool {
spent_km_s > budget.total_km_s
}
///|
pub fn accumulate_budgets(budgets : Array[DeltaVBudget]) -> DeltaVBudget {
let planned = budgets.fold(init=0.0, (total, budget) => {
total + budget.planned_km_s
})
let total = budgets.fold(init=0.0, (sum, budget) => sum + budget.total_km_s)
let reserve = total - planned
{
planned_km_s: planned,
reserve_fraction: if planned == 0.0 {
0.0
} else {
reserve / planned
},
reserve_km_s: reserve,
total_km_s: total,
}
}
///|
pub fn estimate_orbit_pass(
station : GroundStation,
elements : ClassicalElements,
start_epoch : Epoch,
duration_s : Double,
step_s : Double,
min_elevation_rad : Double,
) -> Array[PassWindow] {
let windows : Array[PassWindow] = []
if duration_s <= 0.0 || step_s <= 0.0 {
return windows
}
let mut in_pass = false
let mut rise = 0.0
let mut peak = -half_pi
let mut samples = 0
let mut time = 0.0
while time <= duration_s {
let propagated = propagate_kepler(earth_mu_km3_s2, elements, time)
let state = elements_to_state(earth_mu_km3_s2, propagated)
let sample = sample_visibility(station, start_epoch, state.position_km)
if sample.visible && sample.look.elevation_rad >= min_elevation_rad {
if !in_pass {
in_pass = true
rise = time
peak = sample.look.elevation_rad
samples = 0
}
peak = peak.max(sample.look.elevation_rad)
samples += 1
} else if in_pass {
windows.push({
rise_time_s: rise,
set_time_s: time,
max_elevation_rad: peak,
samples,
})
in_pass = false
}
time += step_s
}
if in_pass {
windows.push({
rise_time_s: rise,
set_time_s: duration_s,
max_elevation_rad: peak,
samples,
})
}
windows
}
///|
pub fn refine_pass_peak(
station : GroundStation,
elements : ClassicalElements,
epoch : Epoch,
start_s : Double,
end_s : Double,
step_s : Double,
) -> Double {
let mut peak = -half_pi
let mut t = start_s
while t <= end_s {
let state = elements_to_state(
earth_mu_km3_s2,
propagate_kepler(earth_mu_km3_s2, elements, t),
)
let angle = sample_visibility(station, epoch, state.position_km).look.elevation_rad
peak = peak.max(angle)
t += step_s.max(1.0)
}
peak
}
///|
pub fn mission_event_count(
timeline : MissionTimeline,
kind : MissionEventKind,
) -> Int {
timeline.events.fold(init=0, (count, event) => {
if event.kind == kind {
count + 1
} else {
count
}
})
}
///|
pub fn mission_energy_proxy(
mu : Double,
elements : ClassicalElements,
mass_kg : Double,
) -> Double {
mass_kg.max(0.0) *
specific_orbital_energy(mu, elements.semi_major_axis_km).abs()
}
///|
pub fn maneuver_event(
time_s : Double,
label : String,
delta_v_km_s : Double,
) -> MissionEvent {
MissionEvent::new(time_s, Maneuver, label, delta_v_km_s~)
}
///|
pub fn contact_event(
time_s : Double,
label : String,
duration_s : Double,
) -> MissionEvent {
MissionEvent::new(time_s, GroundContact, label, duration_s~)
}
///|
pub fn timeline_report(timeline : MissionTimeline) -> String {
let ordered = timeline.sort_by_time()
"events=\{ordered.events.length()},duration_s=\{ordered.end_time_s},delta_v_km_s=\{ordered.total_delta_v()}"
}