///|
pub(all) enum TraverseDecision {
Allow
Review
Block
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct TraverseProfile {
profile_id : String
label : String
max_grade : Double
caution_grade : Double
max_roughness_m : Double
caution_roughness_m : Double
min_confidence : Double
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct TraverseScore {
profile : TraverseProfile
decision : TraverseDecision
score : Int
reasons : Array[String]
next_action : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct IlluminationConstraint {
constraint_id : String
label : String
power_window_evidence_id : String
power_window_source_path : String
power_window_source_status : String
time_start_utc : String
time_end_utc : String
min_sun_altitude_deg : Double
max_sun_altitude_deg : Double
min_sunlit_hours : Double
max_dark_hours : Double
max_relief_shadow_risk : Double
caution_relief_shadow_risk : Double
min_confidence : Double
has_time_window_ephemeris : Bool
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct LocalHorizonEvidence {
evidence_id : String
route_id : String
site_id : String
source_dataset_id : String
source_tile_id : String
source_path : String
power_window_evidence_id : String
power_window_source_path : String
output_path : String
method_id : String
generated_by : String
rows : Int
cols : Int
cell_size_m : Double
center_elevation_m : Double
max_obstruction_elevation_m : Double
max_horizon_angle_deg : Double
min_sun_altitude_deg : Double
max_sun_altitude_deg : Double
evaluated_sample_count : Int
spherical_sunlit_hours : Double
terrain_sunlit_hours : Double
terrain_shadowed_hours : Double
longest_dark_hours : Double
terrain_available_energy_wh : Double
minimum_solar_clearance_deg : Double
maximum_solar_clearance_deg : Double
azimuth_samples : Array[HorizonAzimuthSample]
confidence : Double
decision : TraverseDecision
reasons : Array[String]
next_action : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct HorizonAzimuthSample {
sector_id : String
label : String
azimuth_center_deg : Double
obstruction_row : Int
obstruction_col : Int
obstruction_elevation_m : Double
distance_m : Double
horizon_angle_deg : Double
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct IlluminationAssessment {
constraint : IlluminationConstraint
evidence_dataset_id : String
evidence_tile_id : String
local_horizon : LocalHorizonEvidence
relief_shadow_risk : Double
confidence : Double
decision : TraverseDecision
reasons : Array[String]
next_action : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct EnergyBudget {
budget_id : String
label : String
power_window_evidence_id : String
power_window_source_path : String
power_window_source_status : String
time_start_utc : String
time_end_utc : String
battery_capacity_wh : Double
reserve_wh : Double
drive_power_w : Double
thermal_survival_power_w : Double
comms_power_w : Double
min_energy_margin_wh : Double
required_sunlit_hours : Double
assumed_dark_survival_hours : Double
verified_available_energy_wh : Double
has_time_window_ephemeris : Bool
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct PowerWindowSourceFile {
file_id : String
label : String
source_url : String
local_path : String
sha256 : String
bytes : Int
role : String
status : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct PowerWindowComputation {
method_id : String
generated_by : String
time_step_minutes : Int
horizon_model : String
rover_power_model : String
status : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct PowerWindowEvidence {
evidence_id : String
source_status : String
source_family_url : String
local_source_path : String
source_sha256 : String
source_bytes : Int
source_files : Array[PowerWindowSourceFile]
computation : PowerWindowComputation
observer_geometry : @lunar_ephemeris.ObserverGeometryTimeline
time_start_utc : String
time_end_utc : String
target_lat_deg : Double
target_lon_deg : Double
min_sun_altitude_deg : Double
max_sun_altitude_deg : Double
sunlit_hours : Double
dark_hours : Double
available_energy_wh : Double
confidence : Double
has_time_window_ephemeris : Bool
reasons : Array[String]
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct EnergyAssessment {
budget : EnergyBudget
route_count : Int
blocked_route_count : Int
estimated_drive_hours : Double
estimated_dark_survival_hours : Double
required_energy_wh : Double
available_energy_wh : Double
margin_wh : Double
decision : TraverseDecision
reasons : Array[String]
next_action : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct EnergyRemediationEvidence {
evidence_id : String
site_id : String
route_id : String
power_window_evidence_id : String
power_window_source_path : String
terrain_evidence_path : String
horizon_evidence_path : String
output_path : String
method_id : String
generated_by : String
source_budget_id : String
route_count_before_bound : Int
selected_route_count : Int
blocked_route_count : Int
all_route_required_energy_wh : Double
bounded_required_energy_wh : Double
verified_available_energy_wh : Double
all_route_margin_wh : Double
bounded_margin_wh : Double
minimum_margin_wh : Double
margin_gap_wh : Double
route_count_demand_reduction_wh : Double
selected_route_drive_hours : Double
dark_survival_hours : Double
reserve_wh : Double
drive_energy_wh : Double
comms_energy_wh : Double
thermal_survival_energy_wh : Double
max_dark_survival_hours_for_min_margin : Double
dark_survival_reduction_needed_hours : Double
confidence : Double
decision : TraverseDecision
reasons : Array[String]
next_action : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct RouteCandidate {
route_id : String
label : String
strategy : String
evidence_dataset_id : String
evidence_tile_id : String
evidence_source_path : String
evidence_summary : String
terrain_remediation : TerrainRemediationEvidence
expected_max_grade : Double
expected_roughness_m : Double
confidence : Double
illumination : IlluminationAssessment
decision : TraverseDecision
score : Int
reasons : Array[String]
next_action : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct CorridorScanWindow {
scan_id : String
window_id : String
rank : Int
row_offset : Int
col_offset : Int
source_row_start : Int
source_col_start : Int
min_elevation_m : Double
max_elevation_m : Double
elevation_range_m : Double
max_neighbor_grade : Double
roughness_m : Double
decision : TraverseDecision
selected_route_id : String
note : String
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn conservative_rover_profile() -> TraverseProfile {
{
profile_id: "conservative-lunar-rover-v1",
label: "Conservative Lunar Rover",
max_grade: 0.35,
caution_grade: 0.18,
max_roughness_m: 2.5,
caution_roughness_m: 1.2,
min_confidence: 0.7,
}
}
///|
pub fn traverse_decision_label(decision : TraverseDecision) -> String {
match decision {
Allow => "allow"
Review => "review"
Block => "block"
}
}
///|
pub fn route_candidate_status_label(candidate : RouteCandidate) -> String {
traverse_decision_label(candidate.decision)
}
///|
pub fn corridor_scan_window_status_label(window : CorridorScanWindow) -> String {
traverse_decision_label(window.decision)
}
///|
pub fn illumination_decision_label(
assessment : IlluminationAssessment,
) -> String {
traverse_decision_label(assessment.decision)
}
///|
pub fn energy_decision_label(assessment : EnergyAssessment) -> String {
traverse_decision_label(assessment.decision)
}
///|
pub fn energy_remediation_decision_label(
evidence : EnergyRemediationEvidence,
) -> String {
traverse_decision_label(evidence.decision)
}
///|
pub fn first_trusted_square_power_window_evidence() -> PowerWindowEvidence {
generated_first_trusted_square_power_window_evidence()
}
///|
pub fn first_trusted_square_northeast_stepout_horizon_evidence() -> LocalHorizonEvidence {
let power = first_trusted_square_power_window_evidence()
bounded_local_horizon_evidence(
@terrain.first_trusted_square_northeast_stepout_grid(),
conservative_illumination_constraint(),
power.observer_geometry.hourly_solar_track,
)
}
///|
pub fn first_trusted_square_northeast_stepout_horizon_json() -> Json {
first_trusted_square_northeast_stepout_horizon_evidence().to_json()
}
///|
pub fn first_trusted_square_northeast_stepout_horizon_markdown() -> String {
let evidence = first_trusted_square_northeast_stepout_horizon_evidence()
let mut text = "# Selected Route Local Horizon\n\n"
text += "- evidence: \{evidence.evidence_id}\n"
text += "- site: \{evidence.site_id}\n"
text += "- route: \{evidence.route_id}\n"
text += "- source: \{evidence.source_path}\n"
text += "- power window: \{evidence.power_window_evidence_id}\n"
text += "- method: \{evidence.method_id}\n"
text += "- decision: \{traverse_decision_label(evidence.decision)}\n"
text += "- max horizon angle: \{evidence.max_horizon_angle_deg} deg\n"
text += "- max sun altitude: \{evidence.max_sun_altitude_deg} deg\n"
text += "- evaluated hourly samples: \{evidence.evaluated_sample_count}\n"
text += "- terrain-visible sunlight: \{evidence.terrain_sunlit_hours} h\n"
text += "- terrain-shadowed daylight: \{evidence.terrain_shadowed_hours} h\n"
text += "- longest darkness: \{evidence.longest_dark_hours} h\n"
text += "- route-available energy: \{evidence.terrain_available_energy_wh} Wh\n"
text += "- solar clearance range: \{evidence.minimum_solar_clearance_deg} to \{evidence.maximum_solar_clearance_deg} deg\n"
text += "- confidence: \{evidence.confidence}\n\n"
text += "## Azimuth Profile\n\n"
for sample in evidence.azimuth_samples {
text += "- \{sample.label} (\{sample.azimuth_center_deg} deg): \{sample.horizon_angle_deg} deg at r\{sample.obstruction_row} c\{sample.obstruction_col}\n"
}
text += "\n"
text += "## Reasons\n\n"
for reason in evidence.reasons {
text += "- \{reason}\n"
}
text += "\n## Next Action\n\n"
text += evidence.next_action
text += "\n"
text
}
///|
pub fn first_trusted_square_energy_remediation() -> EnergyRemediationEvidence {
energy_remediation_for_selected_route(
first_trusted_square_energy_assessment(),
"northeast-stepout",
"artifact/mission/first_trusted_square_northeast_stepout_terrain_remediation.json",
first_trusted_square_northeast_stepout_horizon_evidence().output_path,
"artifact/mission/first_trusted_square_energy_remediation.json",
)
}
///|
pub fn first_trusted_square_energy_remediation_json() -> Json {
first_trusted_square_energy_remediation().to_json()
}
///|
pub fn first_trusted_square_energy_remediation_markdown() -> String {
let evidence = first_trusted_square_energy_remediation()
let mut text = "# Selected Route Energy Remediation\n\n"
text += "- evidence: \{evidence.evidence_id}\n"
text += "- site: \{evidence.site_id}\n"
text += "- route: \{evidence.route_id}\n"
text += "- power window: \{evidence.power_window_evidence_id}\n"
text += "- decision: \{energy_remediation_decision_label(evidence)}\n"
text += "- all-route required energy: \{evidence.all_route_required_energy_wh} Wh\n"
text += "- bounded required energy: \{evidence.bounded_required_energy_wh} Wh\n"
text += "- verified available energy: \{evidence.verified_available_energy_wh} Wh\n"
text += "- bounded margin: \{evidence.bounded_margin_wh} Wh\n"
text += "- margin gap: \{evidence.margin_gap_wh} Wh\n"
text += "- selected-route drive hours: \{evidence.selected_route_drive_hours}\n"
text += "- dark survival hours: \{evidence.dark_survival_hours}\n"
text += "- route-count demand reduction: \{evidence.route_count_demand_reduction_wh} Wh\n\n"
text += "## Demand Components\n\n"
text += "- reserve: \{evidence.reserve_wh} Wh\n"
text += "- drive: \{evidence.drive_energy_wh} Wh\n"
text += "- comms: \{evidence.comms_energy_wh} Wh\n"
text += "- thermal survival: \{evidence.thermal_survival_energy_wh} Wh\n\n"
text += "## Reasons\n\n"
for reason in evidence.reasons {
text += "- \{reason}\n"
}
text += "\n## Next Action\n\n"
text += evidence.next_action
text += "\n"
text
}
///|
fn verified_available_energy_wh(evidence : PowerWindowEvidence) -> Double {
if !evidence.has_time_window_ephemeris {
0.0
} else {
evidence.available_energy_wh
}
}
///|
pub fn conservative_illumination_constraint() -> IlluminationConstraint {
let evidence = first_trusted_square_power_window_evidence()
{
constraint_id: "conservative-south-pole-power-v1",
label: "Conservative South Pole Power/Thermal Gate",
power_window_evidence_id: evidence.evidence_id,
power_window_source_path: evidence.local_source_path,
power_window_source_status: evidence.source_status,
time_start_utc: evidence.time_start_utc,
time_end_utc: evidence.time_end_utc,
min_sun_altitude_deg: evidence.min_sun_altitude_deg,
max_sun_altitude_deg: evidence.max_sun_altitude_deg,
min_sunlit_hours: 4.0,
max_dark_hours: 2.0,
max_relief_shadow_risk: 0.22,
caution_relief_shadow_risk: 0.14,
min_confidence: 0.7,
has_time_window_ephemeris: evidence.has_time_window_ephemeris,
}
}
///|
pub fn conservative_energy_budget() -> EnergyBudget {
let evidence = first_trusted_square_power_window_evidence()
let assumed_dark_survival_hours = if evidence.has_time_window_ephemeris {
evidence.dark_hours
} else {
2.0
}
{
budget_id: "conservative-south-pole-energy-v1",
label: "Conservative South Pole Rover Energy Budget",
power_window_evidence_id: evidence.evidence_id,
power_window_source_path: evidence.local_source_path,
power_window_source_status: evidence.source_status,
time_start_utc: evidence.time_start_utc,
time_end_utc: evidence.time_end_utc,
battery_capacity_wh: 1200.0,
reserve_wh: 600.0,
drive_power_w: 120.0,
thermal_survival_power_w: 35.0,
comms_power_w: 20.0,
min_energy_margin_wh: 250.0,
required_sunlit_hours: 4.0,
assumed_dark_survival_hours,
verified_available_energy_wh: verified_available_energy_wh(evidence),
has_time_window_ephemeris: evidence.has_time_window_ephemeris,
}
}
///|
fn add_reason_if(
reasons : Array[String],
condition : Bool,
reason : String,
) -> Unit {
if condition {
reasons.push(reason)
}
}
///|
fn score_from_reasons(
decision : TraverseDecision,
reasons : Array[String],
) -> Int {
match decision {
Allow => 90
Review => 70 - reasons.length() * 8
Block => 30 - reasons.length() * 5
}
}
///|
pub fn score_traverse(
metrics : @terrain.TerrainMetrics,
profile : TraverseProfile,
) -> TraverseScore {
let reasons : Array[String] = []
let grade_blocked = metrics.max_neighbor_grade >= profile.max_grade
let roughness_blocked = metrics.roughness_m >= profile.max_roughness_m
let grade_review = metrics.max_neighbor_grade >= profile.caution_grade
let roughness_review = metrics.roughness_m >= profile.caution_roughness_m
let confidence_review = metrics.uncertainty.confidence <
profile.min_confidence
add_reason_if(
reasons, grade_blocked, "max neighbor grade exceeds rover hard limit",
)
add_reason_if(
reasons, roughness_blocked, "roughness exceeds rover hard limit",
)
add_reason_if(
reasons,
!grade_blocked && grade_review,
"max neighbor grade needs route review",
)
add_reason_if(
reasons,
!roughness_blocked && roughness_review,
"roughness needs route review",
)
add_reason_if(
reasons, confidence_review, "terrain confidence below traverse threshold",
)
let decision = if grade_blocked || roughness_blocked {
Block
} else if grade_review ||
roughness_review ||
confidence_review ||
metrics.hazard_class == @terrain.HazardClass::Caution {
Review
} else {
Allow
}
{
profile,
decision,
score: score_from_reasons(decision, reasons),
reasons,
next_action: match decision {
Allow => "candidate route may advance to simulation"
Review =>
"operator should review route and source confidence before simulation"
Block => "choose alternate route or improve terrain evidence"
},
}
}
///|
pub fn first_trusted_square_traverse_score() -> TraverseScore {
score_traverse(
@terrain.first_trusted_square_metrics(),
conservative_rover_profile(),
)
}
///|
fn combine_decisions(
terrain_decision : TraverseDecision,
illumination_decision : TraverseDecision,
) -> TraverseDecision {
if terrain_decision == Block || illumination_decision == Block {
Block
} else if terrain_decision == Review || illumination_decision == Review {
Review
} else {
Allow
}
}
///|
fn route_candidate_next_action(
terrain_decision : TraverseDecision,
illumination : IlluminationAssessment,
terrain_action : String,
) -> String {
match (terrain_decision, illumination.decision) {
(Allow, Allow) | (Block, Allow) | (Review, Allow) => terrain_action
(Allow, Block) | (Allow, Review) => illumination.next_action
(Block, Block) | (Block, Review) | (Review, Block) | (Review, Review) =>
"\{terrain_action}; \{illumination.next_action}"
}
}
///|
fn route_score(
decision : TraverseDecision,
confidence : Double,
illumination : IlluminationAssessment,
) -> Int {
let base = match decision {
Allow => 88
Review => 62
Block => 18
}
let confidence_penalty = if confidence < 0.5 {
14
} else if confidence < 0.7 {
8
} else {
0
}
let illumination_penalty = match illumination.decision {
Allow => 0
Review => 6
Block => 12
}
base - confidence_penalty - illumination_penalty
}
///|
fn relief_shadow_risk(
grid : @terrain.TerrainGrid,
metrics : @terrain.TerrainMetrics,
) -> Double {
let span = (grid.rows + grid.cols).to_double() * grid.cell_size_m
if span <= 0.0 {
1.0
} else {
metrics.elevation_range_m / span
}
}
///|
fn local_horizon_evidence(
grid : @terrain.TerrainGrid,
constraint : IlluminationConstraint,
solar_track : @lunar_ephemeris.SolarGeometryTrack,
) -> LocalHorizonEvidence {
bounded_local_horizon_evidence(grid, constraint, solar_track)
}
///|
pub fn assess_illumination(
grid : @terrain.TerrainGrid,
constraint : IlluminationConstraint,
solar_track : @lunar_ephemeris.SolarGeometryTrack,
) -> IlluminationAssessment {
let metrics = @terrain.analyze(grid)
let risk = relief_shadow_risk(grid, metrics)
let local_horizon = local_horizon_evidence(grid, constraint, solar_track)
let confidence = metrics.uncertainty.confidence * 0.6
let reasons : Array[String] = []
let missing_ephemeris = !constraint.has_time_window_ephemeris
let risk_blocked = risk >= constraint.max_relief_shadow_risk
let risk_review = risk >= constraint.caution_relief_shadow_risk
let horizon_blocked = local_horizon.decision == Block
let horizon_review = local_horizon.decision == Review
let confidence_review = confidence < constraint.min_confidence
add_reason_if(
reasons, missing_ephemeris, "no time-windowed solar ephemeris is attached",
)
add_reason_if(
reasons,
missing_ephemeris,
"power-window evidence \{constraint.power_window_evidence_id} has source status \{constraint.power_window_source_status}",
)
add_reason_if(
reasons, risk_blocked, "local relief shadow proxy exceeds conservative limit",
)
add_reason_if(
reasons,
horizon_blocked,
"hourly route exposure has \{local_horizon.terrain_sunlit_hours} sunlit hours and \{local_horizon.longest_dark_hours} continuous dark hours",
)
add_reason_if(
reasons,
!risk_blocked && risk_review,
"local relief shadow proxy needs illumination review",
)
add_reason_if(
reasons,
!horizon_blocked && horizon_review,
"bounded local horizon evidence needs illumination review",
)
add_reason_if(
reasons, confidence_review, "illumination confidence below power planning threshold",
)
let decision = if missing_ephemeris || risk_blocked || horizon_blocked {
Block
} else if risk_review || horizon_review || confidence_review {
Review
} else {
Allow
}
{
constraint,
evidence_dataset_id: grid.source_manifest.dataset_id,
evidence_tile_id: grid.tile_id,
local_horizon,
relief_shadow_risk: risk,
confidence,
decision,
reasons,
next_action: match decision {
Allow => "illumination gate may advance to energy simulation"
Review => "review illumination window before route simulation"
Block =>
if missing_ephemeris {
"attach time-windowed solar ephemeris and widen terrain evidence before route simulation"
} else if horizon_blocked {
local_horizon.next_action
} else {
"widen local horizon and terrain-shadow evidence before route simulation"
}
},
}
}
///|
fn blocked_route_count(routes : Array[RouteCandidate]) -> Int {
let mut count = 0
for route in routes {
if route.decision == Block {
count += 1
}
}
count
}
///|
pub fn assess_energy_window(
routes : Array[RouteCandidate],
budget : EnergyBudget,
) -> EnergyAssessment {
let route_count = routes.length()
let blocked_count = blocked_route_count(routes)
let estimated_drive_hours = route_count.to_double() * 0.25
let estimated_dark_survival_hours = budget.assumed_dark_survival_hours
let required_energy_wh = budget.reserve_wh +
budget.drive_power_w * estimated_drive_hours +
budget.comms_power_w * estimated_drive_hours +
budget.thermal_survival_power_w * estimated_dark_survival_hours
let available_energy_wh = if !budget.has_time_window_ephemeris {
0.0
} else if budget.verified_available_energy_wh > budget.battery_capacity_wh {
budget.battery_capacity_wh
} else {
budget.verified_available_energy_wh
}
let margin_wh = available_energy_wh - required_energy_wh
let reasons : Array[String] = []
add_reason_if(
reasons,
!budget.has_time_window_ephemeris,
"no time-windowed solar ephemeris is attached for energy planning",
)
add_reason_if(
reasons,
!budget.has_time_window_ephemeris,
"power-window evidence \{budget.power_window_evidence_id} has source status \{budget.power_window_source_status}",
)
add_reason_if(
reasons,
margin_wh < 0.0,
"estimated energy demand exceeds verified available energy",
)
add_reason_if(
reasons,
margin_wh >= 0.0 && margin_wh < budget.min_energy_margin_wh,
"estimated energy margin is below conservative reserve",
)
add_reason_if(
reasons,
blocked_count > 0,
"\{blocked_count} route candidates remain blocked before energy simulation",
)
let decision = if !budget.has_time_window_ephemeris || margin_wh < 0.0 {
Block
} else if margin_wh < budget.min_energy_margin_wh || blocked_count > 0 {
Review
} else {
Allow
}
{
budget,
route_count,
blocked_route_count: blocked_count,
estimated_drive_hours,
estimated_dark_survival_hours,
required_energy_wh,
available_energy_wh,
margin_wh,
decision,
reasons,
next_action: match decision {
Allow => "energy window may advance to route simulation"
Review => "review route blockers and energy margin before simulation"
Block =>
if !budget.has_time_window_ephemeris {
"attach time-windowed solar ephemeris and verify rover energy budget before simulation"
} else {
"revise rover power model, route count, or site window before simulation"
}
},
}
}
///|
pub fn energy_remediation_for_selected_route(
energy : EnergyAssessment,
route_id : String,
terrain_evidence_path : String,
horizon_evidence_path : String,
output_path : String,
) -> EnergyRemediationEvidence {
let budget = energy.budget
let selected_route_drive_hours = 0.25
let drive_energy_wh = budget.drive_power_w * selected_route_drive_hours
let comms_energy_wh = budget.comms_power_w * selected_route_drive_hours
let thermal_survival_energy_wh = budget.thermal_survival_power_w *
energy.estimated_dark_survival_hours
let bounded_required_energy_wh = budget.reserve_wh +
drive_energy_wh +
comms_energy_wh +
thermal_survival_energy_wh
let bounded_margin_wh = energy.available_energy_wh -
bounded_required_energy_wh
let margin_gap_wh = if bounded_margin_wh >= budget.min_energy_margin_wh {
0.0
} else {
budget.min_energy_margin_wh - bounded_margin_wh
}
let route_count_demand_reduction_wh = energy.required_energy_wh -
bounded_required_energy_wh
let raw_max_dark_survival_hours_for_min_margin = (
energy.available_energy_wh -
budget.reserve_wh -
drive_energy_wh -
comms_energy_wh -
budget.min_energy_margin_wh
) /
budget.thermal_survival_power_w
let max_dark_survival_hours_for_min_margin = if raw_max_dark_survival_hours_for_min_margin <=
0.0 {
0.0
} else {
raw_max_dark_survival_hours_for_min_margin
}
let dark_survival_reduction_needed_hours = if max_dark_survival_hours_for_min_margin >=
energy.estimated_dark_survival_hours {
0.0
} else if max_dark_survival_hours_for_min_margin <= 0.0 {
energy.estimated_dark_survival_hours
} else {
energy.estimated_dark_survival_hours -
max_dark_survival_hours_for_min_margin
}
let reasons : Array[String] = []
add_reason_if(
reasons,
!budget.has_time_window_ephemeris,
"no time-windowed solar ephemeris is attached for bounded energy remediation",
)
add_reason_if(
reasons,
bounded_margin_wh < 0.0,
"selected-route bounded demand still exceeds verified available energy by \{0.0 - bounded_margin_wh} Wh",
)
add_reason_if(
reasons,
bounded_margin_wh >= 0.0 && bounded_margin_wh < budget.min_energy_margin_wh,
"selected-route bounded margin is below the minimum margin by \{margin_gap_wh} Wh",
)
add_reason_if(
reasons,
route_count_demand_reduction_wh > 0.0,
"bounding demand from \{energy.route_count} candidate routes to one selected route removes \{route_count_demand_reduction_wh} Wh but does not clear the gate",
)
add_reason_if(
reasons,
thermal_survival_energy_wh > energy.available_energy_wh,
"thermal survival demand \{thermal_survival_energy_wh} Wh exceeds verified available energy \{energy.available_energy_wh} Wh",
)
add_reason_if(
reasons,
energy.blocked_route_count > 0,
"\{energy.blocked_route_count} route candidates remain blocked before energy can move out of review",
)
let decision = if !budget.has_time_window_ephemeris || bounded_margin_wh < 0.0 {
Block
} else if bounded_margin_wh < budget.min_energy_margin_wh ||
energy.blocked_route_count > 0 {
Review
} else {
Allow
}
{
evidence_id: "first-trusted-square-energy-remediation-v1",
site_id: "first-trusted-square",
route_id,
power_window_evidence_id: budget.power_window_evidence_id,
power_window_source_path: budget.power_window_source_path,
terrain_evidence_path,
horizon_evidence_path,
output_path,
method_id: "bounded-selected-route-energy-remediation-v1",
generated_by: "src/mission/traverse.mbt",
source_budget_id: budget.budget_id,
route_count_before_bound: energy.route_count,
selected_route_count: 1,
blocked_route_count: energy.blocked_route_count,
all_route_required_energy_wh: energy.required_energy_wh,
bounded_required_energy_wh,
verified_available_energy_wh: energy.available_energy_wh,
all_route_margin_wh: energy.margin_wh,
bounded_margin_wh,
minimum_margin_wh: budget.min_energy_margin_wh,
margin_gap_wh,
route_count_demand_reduction_wh,
selected_route_drive_hours,
dark_survival_hours: energy.estimated_dark_survival_hours,
reserve_wh: budget.reserve_wh,
drive_energy_wh,
comms_energy_wh,
thermal_survival_energy_wh,
max_dark_survival_hours_for_min_margin,
dark_survival_reduction_needed_hours,
confidence: if decision == Allow {
0.78
} else {
0.66
},
decision,
reasons,
next_action: match decision {
Allow =>
"selected-route energy remediation clears the conservative power-demand gate"
Review =>
"review route blockers and bounded energy margin before simulation"
Block =>
"reduce reserve or dark-survival demand, increase verified power-window energy, or keep northeast-stepout out of Simulation"
},
}
}
///|
pub fn first_trusted_square_energy_assessment() -> EnergyAssessment {
assess_energy_window(
first_trusted_square_route_candidates(),
conservative_energy_budget(),
)
}
///|
fn make_route_candidate(
route_id : String,
label : String,
strategy : String,
grid : @terrain.TerrainGrid,
profile : TraverseProfile,
illumination_constraint : IlluminationConstraint,
solar_track : @lunar_ephemeris.SolarGeometryTrack,
extra_reason : String,
next_action : String,
) -> RouteCandidate {
let metrics = @terrain.analyze(grid)
let reasons : Array[String] = []
let expected_max_grade = metrics.max_neighbor_grade
let expected_roughness_m = metrics.roughness_m
let confidence = metrics.uncertainty.confidence
let terrain_remediation = terrain_remediation_evidence(
route_id, grid, metrics, profile,
)
let illumination = assess_illumination(
grid, illumination_constraint, solar_track,
)
let grade_blocked = expected_max_grade >= profile.max_grade
let roughness_blocked = expected_roughness_m >= profile.max_roughness_m
let grade_review = expected_max_grade >= profile.caution_grade
let roughness_review = expected_roughness_m >= profile.caution_roughness_m
let confidence_review = confidence < profile.min_confidence
add_reason_if(
reasons, grade_blocked, "expected grade exceeds rover hard limit",
)
add_reason_if(
reasons, roughness_blocked, "expected roughness exceeds rover hard limit",
)
add_reason_if(
reasons,
!grade_blocked && grade_review,
"expected grade needs route review",
)
add_reason_if(
reasons,
!roughness_blocked && roughness_review,
"expected roughness needs route review",
)
add_reason_if(reasons, confidence_review, "route confidence below threshold")
if extra_reason != "" {
reasons.push(extra_reason)
}
let terrain_decision = terrain_remediation.decision
let decision = combine_decisions(terrain_decision, illumination.decision)
if illumination.decision != Allow {
reasons.push(
"illumination gate is \{illumination_decision_label(illumination)}",
)
}
{
route_id,
label,
strategy,
evidence_dataset_id: grid.source_manifest.dataset_id,
evidence_tile_id: grid.tile_id,
evidence_source_path: grid.source_manifest.source_path,
evidence_summary: "\{@terrain.hazard_label(metrics.hazard_class)} measured window; elevation range \{metrics.elevation_range_m} m",
terrain_remediation,
expected_max_grade,
expected_roughness_m,
confidence,
illumination,
decision,
score: route_score(decision, confidence, illumination),
reasons,
next_action: route_candidate_next_action(
terrain_decision, illumination, next_action,
),
}
}
///|
pub fn first_trusted_square_route_candidates() -> Array[RouteCandidate] {
let profile = conservative_rover_profile()
let illumination_constraint = conservative_illumination_constraint()
let solar_track = first_trusted_square_power_window_evidence().observer_geometry.hourly_solar_track
[
make_route_candidate(
"direct-lola-window",
"Direct traverse across measured LOLA patch",
"Use the active 4x4 measured window as-is.",
@terrain.first_trusted_square_grid(),
profile,
illumination_constraint,
solar_track,
"active LOLA patch is blocked",
"do not traverse directly; use this as the baseline hazard case",
),
make_route_candidate(
"west-contour-detour",
"West contour detour candidate",
"Test the west-adjacent LOLA window as a possible contour route around the active patch.",
@terrain.first_trusted_square_west_contour_grid(),
profile,
illumination_constraint,
solar_track,
"west-adjacent LOLA window is measured but still blocked at this scale",
"widen the west corridor extraction before simulation",
),
make_route_candidate(
"north-rim-stepout",
"North rim step-out candidate",
"Test the north-adjacent LOLA window as a possible step-out toward a smoother rim approach.",
@terrain.first_trusted_square_north_rim_grid(),
profile,
illumination_constraint,
solar_track,
"north-adjacent LOLA window is measured but still blocked at this scale",
"widen the north corridor extraction before simulation",
),
make_route_candidate(
"southwest-bypass",
"Southwest widened bypass candidate",
"Use the lowest-grade measured window from the first 5x5 corridor scan as a possible bypass direction.",
@terrain.first_trusted_square_southwest_bypass_grid(),
profile,
illumination_constraint,
solar_track,
"southwest widened corridor is measured and lower risk but still blocked",
"continue corridor search beyond the southwest bypass before simulation",
),
make_route_candidate(
"south-stepout",
"South step-out candidate",
"Use the south measured window from the first 5x5 corridor scan as a second widened route direction.",
@terrain.first_trusted_square_south_stepout_grid(),
profile,
illumination_constraint,
solar_track,
"south widened corridor is measured and smoother than the active patch but still blocked",
"continue south corridor extraction before simulation",
),
make_route_candidate(
"northeast-stepout",
"Northeast 9x9 step-out candidate",
"Promote the best measured window from the active 9x9 corridor scan into a named route fixture.",
@terrain.first_trusted_square_northeast_stepout_grid(),
profile,
illumination_constraint,
solar_track,
"best active 9x9 corridor window is measured and selected but still blocked",
"review the promoted route fixture before simulation",
),
]
}
///|
pub fn first_trusted_square_corridor_scan() -> Array[CorridorScanWindow] {
generated_first_trusted_square_corridor_scan()
}
///|
pub fn first_trusted_square_best_corridor_window() -> CorridorScanWindow {
first_trusted_square_corridor_scan()[0]
}