///|
pub struct WindowConstraint {
minimum_elevation_rad : Double
maximum_range_km : Double
minimum_duration_s : Double
required_score : Double
} derive(Debug, Eq)
///|
pub struct PlannedWindow {
window : VisibilityWindow
score : Double
data_volume_mb : Double
selected : Bool
} derive(Debug, Eq)
///|
pub struct WindowSchedule {
windows : Array[PlannedWindow]
total_duration_s : Double
total_volume_mb : Double
mean_score : Double
} derive(Debug, Eq)
///|
pub fn make_visibility_window(
start : Epoch,
end : Epoch,
max_elevation_rad : Double,
closest_range_km : Double,
) -> VisibilityWindow {
{ start, end, max_elevation_rad, closest_range_km }
}
///|
pub fn WindowConstraint::new(
minimum_elevation_rad : Double,
maximum_range_km : Double,
minimum_duration_s : Double,
) -> WindowConstraint {
{
minimum_elevation_rad,
maximum_range_km: maximum_range_km.max(0.0),
minimum_duration_s: minimum_duration_s.max(0.0),
required_score: 0.0,
}
}
///|
pub fn WindowConstraint::with_score(
constraint : WindowConstraint,
required_score : Double,
) -> WindowConstraint {
{ ..constraint, required_score: required_score.max(0.0) }
}
///|
pub fn window_duration_s(window : VisibilityWindow) -> Double {
(window.end.seconds_since_j2000 - window.start.seconds_since_j2000).max(0.0)
}
///|
pub fn window_midpoint(window : VisibilityWindow) -> Epoch {
window.start.add_seconds(window_duration_s(window) / 2.0)
}
///|
pub fn window_is_acceptable(
window : VisibilityWindow,
constraint : WindowConstraint,
) -> Bool {
window_duration_s(window) >= constraint.minimum_duration_s &&
window.max_elevation_rad >= constraint.minimum_elevation_rad &&
window.closest_range_km <= constraint.maximum_range_km
}
///|
pub fn window_quality_score(
window : VisibilityWindow,
constraint : WindowConstraint,
) -> Double {
let duration_score = if constraint.minimum_duration_s == 0.0 {
1.0
} else {
window_duration_s(window) / constraint.minimum_duration_s
}
let elevation_score = if constraint.minimum_elevation_rad.abs() < 1.0e-12 {
1.0
} else {
window.max_elevation_rad / constraint.minimum_elevation_rad
}
let range_score = if constraint.maximum_range_km <= 0.0 {
0.0
} else {
(1.0 - window.closest_range_km / constraint.maximum_range_km).max(0.0)
}
(0.5 * duration_score + 0.3 * elevation_score + 0.2 * range_score).max(0.0)
}
///|
pub fn window_data_volume_mb(
window : VisibilityWindow,
rate_kbps : Double,
) -> Double {
window_duration_s(window) * rate_kbps.max(0.0) / 8000.0
}
///|
pub fn schedule_window(
window : VisibilityWindow,
constraint : WindowConstraint,
rate_kbps : Double,
) -> PlannedWindow {
let score = window_quality_score(window, constraint)
{
window,
score,
data_volume_mb: window_data_volume_mb(window, rate_kbps),
selected: window_is_acceptable(window, constraint) &&
score >= constraint.required_score,
}
}
///|
fn planned_start(item : PlannedWindow) -> Double {
item.window.start.seconds_since_j2000
}
///|
fn sort_planned_windows(windows : Array[PlannedWindow]) -> Array[PlannedWindow] {
let result = windows.copy()
for i in 0.. Bool {
a.start.seconds_since_j2000 < b.end.seconds_since_j2000 &&
b.start.seconds_since_j2000 < a.end.seconds_since_j2000
}
///|
fn select_non_overlapping(
candidates : Array[PlannedWindow],
) -> Array[PlannedWindow] {
let sorted = sort_planned_windows(candidates)
let selected : Array[PlannedWindow] = []
for candidate in sorted {
if !candidate.selected {
continue
}
let conflict = selected.any(item => overlaps(item.window, candidate.window))
if !conflict {
selected.push(candidate)
}
}
selected
}
///|
pub fn plan_windows(
windows : Array[VisibilityWindow],
constraint : WindowConstraint,
rate_kbps : Double,
) -> WindowSchedule {
let candidates = windows.map(window => {
schedule_window(window, constraint, rate_kbps)
})
let selected = select_non_overlapping(candidates)
let total_duration = selected.fold(init=0.0, (sum, item) => {
sum + window_duration_s(item.window)
})
let total_volume = selected.fold(init=0.0, (sum, item) => {
sum + item.data_volume_mb
})
let score = selected.fold(init=0.0, (sum, item) => sum + item.score)
{
windows: selected,
total_duration_s: total_duration,
total_volume_mb: total_volume,
mean_score: if selected.length() == 0 {
0.0
} else {
score / Double::from_int(selected.length())
},
}
}
///|
pub fn schedule_is_feasible(
schedule : WindowSchedule,
minimum_volume_mb : Double,
) -> Bool {
schedule.total_volume_mb >= minimum_volume_mb.max(0.0)
}
///|
pub fn schedule_efficiency(schedule : WindowSchedule) -> Double {
if schedule.total_duration_s <= 0.0 {
0.0
} else {
schedule.total_volume_mb / schedule.total_duration_s
}
}
///|
pub fn schedule_first_window(schedule : WindowSchedule) -> PlannedWindow? {
if schedule.windows.length() == 0 {
None
} else {
Some(schedule.windows[0])
}
}
///|
pub fn schedule_last_window(schedule : WindowSchedule) -> PlannedWindow? {
if schedule.windows.length() == 0 {
None
} else {
Some(schedule.windows[schedule.windows.length() - 1])
}
}
///|
pub fn schedule_to_mission_timeline(
schedule : WindowSchedule,
label_prefix : String,
) -> MissionTimeline {
let timeline = MissionTimeline::new()
let mut index = 0
let mut result = timeline
for item in schedule.windows {
let label = "\{label_prefix}-\{index}"
result = result.add(
contact_event(
item.window.start.seconds_since_j2000,
label,
window_duration_s(item.window),
),
)
index += 1
}
result.sort_by_time()
}
///|
pub fn schedule_gap_seconds(schedule : WindowSchedule) -> Double {
if schedule.windows.length() < 2 {
0.0
} else {
let mut gap = 0.0
for i in 0..<(schedule.windows.length() - 1) {
let current = schedule.windows[i].window
let next = schedule.windows[i + 1].window
let difference = next.start.seconds_since_j2000 -
current.end.seconds_since_j2000
gap = gap.max(difference)
}
gap
}
}
///|
pub fn schedule_window_count(schedule : WindowSchedule) -> Int {
schedule.windows.length()
}
///|
pub fn schedule_summary(schedule : WindowSchedule) -> String {
"windows=\{schedule.windows.length()},duration_s=\{schedule.total_duration_s},volume_mb=\{schedule.total_volume_mb},mean_score=\{schedule.mean_score}"
}
///|
pub fn filter_windows_by_time(
windows : Array[VisibilityWindow],
start : Epoch,
end : Epoch,
) -> Array[VisibilityWindow] {
windows.filter(window => {
window.start.seconds_since_j2000 >= start.seconds_since_j2000 &&
window.end.seconds_since_j2000 <= end.seconds_since_j2000
})
}
///|
pub fn merge_adjacent_windows(
windows : Array[VisibilityWindow],
maximum_gap_s : Double,
) -> Array[VisibilityWindow] {
if windows.length() == 0 {
[]
} else {
let sorted = windows.copy()
for i in 0.. last.end.seconds_since_j2000 {
window.end
} else {
last.end
},
last.max_elevation_rad.max(window.max_elevation_rad),
last.closest_range_km.min(window.closest_range_km),
)
} else {
merged.push(window)
}
}
}
merged
}
}