From 68d9844d0de80212f3c3ebd5bacaf226dd92b73b Mon Sep 17 00:00:00 2001 From: Mikolaj Wielgus Date: Wed, 9 Jul 2025 11:45:15 +0200 Subject: [PATCH] feat(topola-egui): Add progress bar for the currently routed ratline The capability to measure progress will later be useful to choose slower but better optimization strategies if more time is available. --- crates/topola-egui/src/status_bar.rs | 16 ++++- crates/topola-egui/src/viewport.rs | 3 +- src/autorouter/autoroute.rs | 18 +++++- src/autorouter/compare_detours.rs | 6 +- src/autorouter/execution.rs | 38 +++++++++++- src/autorouter/invoker.rs | 2 +- src/autorouter/measure_length.rs | 4 ++ src/autorouter/place_via.rs | 4 ++ src/autorouter/remove_bands.rs | 5 +- src/interactor/activity.rs | 34 ++++++++++- src/router/ng/router.rs | 6 +- src/router/route.rs | 15 ++++- src/router/router.rs | 2 +- src/router/thetastar.rs | 89 +++++++++++++++++++++------- src/stepper.rs | 34 ++++++++--- 15 files changed, 233 insertions(+), 43 deletions(-) diff --git a/crates/topola-egui/src/status_bar.rs b/crates/topola-egui/src/status_bar.rs index 9ecf9ac..c70c8fa 100644 --- a/crates/topola-egui/src/status_bar.rs +++ b/crates/topola-egui/src/status_bar.rs @@ -4,7 +4,7 @@ use std::ops::ControlFlow; -use topola::interactor::activity::ActivityStepperWithStatus; +use topola::{interactor::activity::ActivityStepperWithStatus, stepper::EstimateProgress}; use crate::{translator::Translator, viewport::Viewport}; @@ -38,6 +38,20 @@ impl StatusBar { "x: {} y: {} \t {}", latest_pos.x, -latest_pos.y, message )); + + if let Some(activity) = maybe_activity { + let value = activity.estimate_progress_value(); + let maximum = activity.estimate_progress_maximum(); + + ui.add( + egui::ProgressBar::new((value / maximum) as f32).text(format!( + "{:.1} ({:.1}/{:.1})", + value / maximum * 100.0, + value, + maximum + )), + ); + } }); } } diff --git a/crates/topola-egui/src/viewport.rs b/crates/topola-egui/src/viewport.rs index 66f98dd..fe43574 100644 --- a/crates/topola-egui/src/viewport.rs +++ b/crates/topola-egui/src/viewport.rs @@ -376,7 +376,6 @@ impl Viewport { }; if menu_bar.show_pathfinding_scores { - //TODO "{astar.scores[index]} ({astar.estimate_scores[index]}) (...)" let score_text = thetastar .scores() .get(&navnode) @@ -384,7 +383,7 @@ impl Viewport { format!("g={:.2}", s) }); let estimate_score_text = thetastar - .estimate_scores() + .cost_to_goal_estimate_scores() .get(&navnode) .map_or_else(String::new, |s| { format!("(f={:.2})", s) diff --git a/src/autorouter/autoroute.rs b/src/autorouter/autoroute.rs index d165117..f842cc4 100644 --- a/src/autorouter/autoroute.rs +++ b/src/autorouter/autoroute.rs @@ -17,7 +17,7 @@ use crate::{ router::{ navcord::Navcord, navmesh::Navmesh, thetastar::ThetastarStepper, RouteStepper, Router, }, - stepper::Step, + stepper::{EstimateProgress, Step}, }; use super::{invoker::GetDebugOverlayData, Autorouter, AutorouterError, AutorouterOptions}; @@ -164,6 +164,22 @@ impl Step, Option, AutorouteContinu } } +impl EstimateProgress for AutorouteExecutionStepper { + type Value = f64; + + fn estimate_progress_value(&self) -> f64 { + self.route + .as_ref() + .map_or(0.0, |route| route.estimate_progress_value()) + } + + fn estimate_progress_maximum(&self) -> f64 { + self.route + .as_ref() + .map_or(0.0, |route| route.estimate_progress_maximum()) + } +} + impl GetDebugOverlayData for AutorouteExecutionStepper { fn maybe_thetastar(&self) -> Option<&ThetastarStepper> { self.route.as_ref().map(|route| route.thetastar()) diff --git a/src/autorouter/compare_detours.rs b/src/autorouter/compare_detours.rs index cbb69d7..6bcd76a 100644 --- a/src/autorouter/compare_detours.rs +++ b/src/autorouter/compare_detours.rs @@ -15,7 +15,7 @@ use crate::{ geometry::{primitive::PrimitiveShape, shape::MeasureLength}, graph::MakeRef, router::{navcord::Navcord, navmesh::Navmesh, thetastar::ThetastarStepper}, - stepper::Step, + stepper::{EstimateProgress, Step}, }; use super::{ @@ -100,6 +100,10 @@ impl Step, (f64, f64)> for CompareDetoursExecut } } +impl EstimateProgress for CompareDetoursExecutionStepper { + type Value = f64; +} + impl GetDebugOverlayData for CompareDetoursExecutionStepper { fn maybe_thetastar(&self) -> Option<&ThetastarStepper> { self.autoroute.maybe_thetastar() diff --git a/src/autorouter/execution.rs b/src/autorouter/execution.rs index 840a30d..61ae470 100644 --- a/src/autorouter/execution.rs +++ b/src/autorouter/execution.rs @@ -11,7 +11,7 @@ use crate::{ board::AccessMesadata, layout::{via::ViaWeight, LayoutEdit}, router::ng, - stepper::{Abort, Step}, + stepper::{Abort, EstimateProgress, Step}, }; use super::{ @@ -165,3 +165,39 @@ impl Abort> for ExecutionStepper { } } } + +// Since enum_dispatch does not really support generics, we implement this the +// long way. +impl EstimateProgress for ExecutionStepper { + type Value = f64; + + fn estimate_progress_value(&self) -> f64 { + match self { + ExecutionStepper::Autoroute(autoroute) => autoroute.estimate_progress_value(), + ExecutionStepper::TopoAutoroute(toporoute) => toporoute.estimate_progress_value(), + ExecutionStepper::PlaceVia(place_via) => place_via.estimate_progress_value(), + ExecutionStepper::RemoveBands(remove_bands) => remove_bands.estimate_progress_value(), + ExecutionStepper::CompareDetours(compare_detours) => { + compare_detours.estimate_progress_value() + } + ExecutionStepper::MeasureLength(measure_length) => { + measure_length.estimate_progress_value() + } + } + } + + fn estimate_progress_maximum(&self) -> f64 { + match self { + ExecutionStepper::Autoroute(autoroute) => autoroute.estimate_progress_maximum(), + ExecutionStepper::TopoAutoroute(toporoute) => toporoute.estimate_progress_maximum(), + ExecutionStepper::PlaceVia(place_via) => place_via.estimate_progress_maximum(), + ExecutionStepper::RemoveBands(remove_bands) => remove_bands.estimate_progress_maximum(), + ExecutionStepper::CompareDetours(compare_detours) => { + compare_detours.estimate_progress_maximum() + } + ExecutionStepper::MeasureLength(measure_length) => { + measure_length.estimate_progress_maximum() + } + } + } +} diff --git a/src/autorouter/invoker.rs b/src/autorouter/invoker.rs index 4112081..530ca1b 100644 --- a/src/autorouter/invoker.rs +++ b/src/autorouter/invoker.rs @@ -150,10 +150,10 @@ impl Invoker { } } - #[debug_requires(self.ongoing_command.is_none())] /// Pass given command to be executed. /// /// Function used to set given [`Command`] to ongoing state, dispatch and execute it. + #[debug_requires(self.ongoing_command.is_none())] pub fn execute_stepper( &mut self, command: Command, diff --git a/src/autorouter/measure_length.rs b/src/autorouter/measure_length.rs index 1ccc269..52b9fcc 100644 --- a/src/autorouter/measure_length.rs +++ b/src/autorouter/measure_length.rs @@ -8,6 +8,7 @@ use crate::{ board::AccessMesadata, geometry::shape::MeasureLength as MeasureLengthTrait, graph::MakeRef, + stepper::EstimateProgress, }; use super::{invoker::GetDebugOverlayData, selection::BandSelection, Autorouter, AutorouterError}; @@ -47,4 +48,7 @@ impl MeasureLengthExecutionStepper { } } +impl EstimateProgress for MeasureLengthExecutionStepper { + type Value = f64; +} impl GetDebugOverlayData for MeasureLengthExecutionStepper {} diff --git a/src/autorouter/place_via.rs b/src/autorouter/place_via.rs index 01a0d44..69c18c0 100644 --- a/src/autorouter/place_via.rs +++ b/src/autorouter/place_via.rs @@ -9,6 +9,7 @@ use crate::{ board::AccessMesadata, layout::{via::ViaWeight, LayoutEdit}, + stepper::EstimateProgress, }; use super::{invoker::GetDebugOverlayData, Autorouter, AutorouterError}; @@ -46,4 +47,7 @@ impl PlaceViaExecutionStepper { } } +impl EstimateProgress for PlaceViaExecutionStepper { + type Value = f64; +} impl GetDebugOverlayData for PlaceViaExecutionStepper {} diff --git a/src/autorouter/remove_bands.rs b/src/autorouter/remove_bands.rs index 0b42860..f67e2de 100644 --- a/src/autorouter/remove_bands.rs +++ b/src/autorouter/remove_bands.rs @@ -4,7 +4,7 @@ //! Provides functionality to remove bands from the layout. -use crate::{board::AccessMesadata, layout::LayoutEdit}; +use crate::{board::AccessMesadata, layout::LayoutEdit, stepper::EstimateProgress}; use super::{invoker::GetDebugOverlayData, selection::BandSelection, Autorouter, AutorouterError}; @@ -44,4 +44,7 @@ impl RemoveBandsExecutionStepper { } } +impl EstimateProgress for RemoveBandsExecutionStepper { + type Value = f64; +} impl GetDebugOverlayData for RemoveBandsExecutionStepper {} diff --git a/src/interactor/activity.rs b/src/interactor/activity.rs index 560da9e..89f53b2 100644 --- a/src/interactor/activity.rs +++ b/src/interactor/activity.rs @@ -25,7 +25,7 @@ use crate::{ ng, thetastar::ThetastarStepper, }, - stepper::{Abort, OnEvent, Step}, + stepper::{Abort, EstimateProgress, OnEvent, Step}, }; /// Stores the interactive input data from the user. @@ -98,6 +98,26 @@ impl Abort> for ActivityStepper { } } +// Since enum_dispatch does not really support generics, we implement this the +// long way. +impl EstimateProgress for ActivityStepper { + type Value = f64; + + fn estimate_progress_value(&self) -> f64 { + match self { + ActivityStepper::Interaction(..) => 0.0, + ActivityStepper::Execution(execution) => execution.estimate_progress_value(), + } + } + + fn estimate_progress_maximum(&self) -> f64 { + match self { + ActivityStepper::Interaction(..) => 0.0, + ActivityStepper::Execution(execution) => execution.estimate_progress_maximum(), + } + } +} + impl OnEvent, InteractiveEvent> for ActivityStepper { type Output = Result<(), InteractionError>; @@ -179,6 +199,18 @@ impl OnEvent, InteractiveEvent } } +impl EstimateProgress for ActivityStepperWithStatus { + type Value = f64; + + fn estimate_progress_value(&self) -> f64 { + self.activity.estimate_progress_value() + } + + fn estimate_progress_maximum(&self) -> f64 { + self.activity.estimate_progress_maximum() + } +} + impl GetDebugOverlayData for ActivityStepperWithStatus { fn maybe_thetastar(&self) -> Option<&ThetastarStepper> { self.activity.maybe_thetastar() diff --git a/src/router/ng/router.rs b/src/router/ng/router.rs index 3bb0e2d..e739e5b 100644 --- a/src/router/ng/router.rs +++ b/src/router/ng/router.rs @@ -15,7 +15,7 @@ use crate::{ geometry::primitive::PrimitiveShape, graph::GenericIndex, layout::{poly::PolyWeight, Layout, LayoutEdit}, - stepper::Abort, + stepper::{Abort, EstimateProgress}, }; use super::{ @@ -232,6 +232,10 @@ impl AutorouteExecutionStepp } } +impl EstimateProgress for AutorouteExecutionStepper { + type Value = f64; +} + impl GetDebugOverlayData for AutorouteExecutionStepper { fn maybe_topo_navmesh(&self) -> Option> { Some(pie::navmesh::NavmeshRef { diff --git a/src/router/route.rs b/src/router/route.rs index 229ee1c..c22c9d7 100644 --- a/src/router/route.rs +++ b/src/router/route.rs @@ -19,7 +19,7 @@ use crate::{ thetastar::{ThetastarError, ThetastarStepper}, Router, RouterThetastarStrategy, }, - stepper::Step, + stepper::{EstimateProgress, Step}, }; #[derive(Getters, Dissolve)] @@ -80,6 +80,7 @@ impl Step, BandTermsegIndex> for RouteStepper { let target = self.thetastar.graph().destination(); let mut strategy = RouterThetastarStrategy::new(layout, &mut self.navcord, target); let result = self.thetastar.step(&mut strategy); + self.ghosts = strategy.probe_ghosts; self.obstacles = strategy.probe_obstacles; @@ -97,3 +98,15 @@ impl Step, BandTermsegIndex> for RouteStepper { } } } + +impl EstimateProgress for RouteStepper { + type Value = f64; + + fn estimate_progress_value(&self) -> f64 { + self.thetastar.estimate_progress_value() + } + + fn estimate_progress_maximum(&self) -> f64 { + self.thetastar.estimate_progress_maximum() + } +} diff --git a/src/router/router.rs b/src/router/router.rs index 8790de3..9a01d77 100644 --- a/src/router/router.rs +++ b/src/router/router.rs @@ -119,7 +119,7 @@ impl ThetastarStrategy self.navcord.step_back(self.layout); } - fn estimate_cost(&mut self, navmesh: &Navmesh, vertex: NavnodeIndex) -> f64 { + fn estimate_cost_to_goal(&mut self, navmesh: &Navmesh, vertex: NavnodeIndex) -> f64 { let start_point = PrimitiveIndex::from(navmesh.node_weight(vertex).unwrap().node) .primitive(self.layout.drawing()) .shape() diff --git a/src/router/thetastar.rs b/src/router/thetastar.rs index f46aa78..06fbfa5 100644 --- a/src/router/thetastar.rs +++ b/src/router/thetastar.rs @@ -11,7 +11,7 @@ use std::collections::btree_map::Entry; use std::collections::{BTreeMap, BinaryHeap}; -use std::ops::ControlFlow; +use std::ops::{ControlFlow, Sub}; use derive_getters::Getters; use petgraph::algo::Measure; @@ -20,7 +20,7 @@ use thiserror::Error; use std::cmp::Ordering; -use crate::stepper::Step; +use crate::stepper::{EstimateProgress, Step}; #[derive(Copy, Clone, Debug)] pub struct MinScored(pub K, pub T); @@ -122,7 +122,7 @@ where ) -> Result, ()>; fn place_probe_to_navnode<'a>(&mut self, graph: &'a G, probed_navnode: G::NodeId) -> Option; fn remove_probe(&mut self, graph: &G); - fn estimate_cost(&mut self, graph: &G, navnode: G::NodeId) -> K; + fn estimate_cost_to_goal(&mut self, graph: &G, navnode: G::NodeId) -> K; } pub trait MakeEdgeRef: IntoEdgeReferences { @@ -155,21 +155,28 @@ where G: GraphBase, G::NodeId: Eq + Ord, for<'a> &'a G: IntoEdges + MakeEdgeRef, - K: Measure + Copy, + K: Measure + Copy + Sub, { state: ThetastarState, graph: G, + /// The priority queue of the navnodes to expand. #[getter(skip)] - visit_next: BinaryHeap>, + frontier: BinaryHeap>, /// Also known as the g-scores, or just g. scores: BTreeMap, /// Also known as the f-scores, or just f. - estimate_scores: BTreeMap, + cost_to_goal_estimate_scores: BTreeMap, #[getter(skip)] path_tracker: PathTracker, // FIXME: To work around edge references borrowing from the graph we collect then reiterate over them. #[getter(skip)] edge_ids: Vec, + + #[getter(skip)] + progress_estimate_value: K, + + #[getter(skip)] + progress_estimate_maximum: K, } #[derive(Error, Debug, Clone)] @@ -183,29 +190,55 @@ where G: GraphBase, G::NodeId: Eq + Ord, for<'a> &'a G: IntoEdges + MakeEdgeRef, - K: Measure + Copy, + K: Measure + Copy + Sub, { pub fn new( graph: G, start: G::NodeId, strategy: &mut impl ThetastarStrategy, ) -> Self { + let estimated_cost_from_start_to_goal = strategy.estimate_cost_to_goal(&graph, start); + let mut this = Self { state: ThetastarState::Scanning, graph, - visit_next: BinaryHeap::new(), + frontier: BinaryHeap::new(), scores: BTreeMap::new(), - estimate_scores: BTreeMap::new(), + cost_to_goal_estimate_scores: BTreeMap::new(), path_tracker: PathTracker::::new(), edge_ids: Vec::new(), + progress_estimate_value: K::default(), + progress_estimate_maximum: estimated_cost_from_start_to_goal, }; let zero_score = K::default(); this.scores.insert(start, zero_score); - this.visit_next - .push(MinScored(strategy.estimate_cost(&this.graph, start), start)); + this.frontier + .push(MinScored(estimated_cost_from_start_to_goal, start)); this } + + fn push_to_frontier( + &mut self, + next: G::NodeId, + next_score: K, + predecessor: G::NodeId, + strategy: &mut impl ThetastarStrategy, + ) { + let cost_to_goal_estimate = strategy.estimate_cost_to_goal(&self.graph, next); + + if cost_to_goal_estimate > self.progress_estimate_maximum { + self.progress_estimate_maximum = cost_to_goal_estimate; + } + + if self.progress_estimate_maximum - cost_to_goal_estimate > self.progress_estimate_value { + self.progress_estimate_value = self.progress_estimate_maximum - cost_to_goal_estimate; + } + + self.path_tracker.set_predecessor(next, predecessor); + let next_estimate_score = next_score + cost_to_goal_estimate; + self.frontier.push(MinScored(next_estimate_score, next)); + } } impl> @@ -214,7 +247,7 @@ where G: GraphBase, G::NodeId: Eq + Ord, for<'a> &'a G: IntoEdges + MakeEdgeRef, - K: Measure + Copy, + K: Measure + Copy + Sub, { type Error = ThetastarError; @@ -227,7 +260,7 @@ where > { match self.state { ThetastarState::Scanning => { - let Some(MinScored(estimate_score, navnode)) = self.visit_next.pop() else { + let Some(MinScored(estimate_score, navnode)) = self.frontier.pop() else { return Err(ThetastarError::NotFound); }; @@ -243,7 +276,7 @@ where return Ok(ControlFlow::Break((cost, path, result))); } - match self.estimate_scores.entry(navnode) { + match self.cost_to_goal_estimate_scores.entry(navnode) { Entry::Occupied(mut entry) => { // If the node has already been visited with an equal or lower // estimated score than now, then we do not need to re-visit it. @@ -299,10 +332,7 @@ where } } - self.path_tracker.set_predecessor(next, parent_navnode); - let next_estimate_score = - next_score + strategy.estimate_cost(&self.graph, next); - self.visit_next.push(MinScored(next_estimate_score, next)); + self.push_to_frontier(next, next_score, parent_navnode, strategy); self.state = ThetastarState::Probing(visited_navnode); Ok(ControlFlow::Continue(self.state)) @@ -353,10 +383,7 @@ where } } - self.path_tracker.set_predecessor(next, visited_navnode); - let next_estimate_score = - next_score + strategy.estimate_cost(&self.graph, next); - self.visit_next.push(MinScored(next_estimate_score, next)); + self.push_to_frontier(next, next_score, visited_navnode, strategy); self.state = ThetastarState::Probing(visited_navnode); Ok(ControlFlow::Continue(self.state)) @@ -374,3 +401,21 @@ where } } } + +impl EstimateProgress for ThetastarStepper +where + G: GraphBase, + G::NodeId: Eq + Ord, + for<'a> &'a G: IntoEdges + MakeEdgeRef, + K: Measure + Copy + Sub, +{ + type Value = K; + + fn estimate_progress_value(&self) -> K { + self.progress_estimate_value + } + + fn estimate_progress_maximum(&self) -> K { + self.progress_estimate_maximum + } +} diff --git a/src/stepper.rs b/src/stepper.rs index 724b1ce..ac4b44c 100644 --- a/src/stepper.rs +++ b/src/stepper.rs @@ -11,14 +11,14 @@ use core::ops::ControlFlow; /// /// An object that implements this trait is called a "stepper". /// -/// Steppers always progress linearly, that is, it is presumed that the context -/// does not change between calls in a way that can affect the stepper's future -/// states. Advanceable data structures designed for uses where the future state -/// intentionally *may* change from the information supplied as arguments are -/// not considered steppers. An example of such an advanceable non-stepper is -/// the [`Navcord`](crate::router::navcord::Navcord) struct, as it does not progress -/// linearly because it branches out by on each call taking in a -/// changeable `to` argument that affects the future states. +/// Steppers always progress linearly and their future states are determined by +/// the initial state. It is assumed that the changes in context cannot change +/// the stepper's execution. Advanceable data structures designed for uses where +/// the future state intentionally *may* change from the information supplied +/// after initialization are not considered steppers. An example of such an +/// advanceable non-stepper is the [`Navcord`] (crate::router::navcord::Navcord) +/// struct, as it does not progress linearly because it branches out on each +/// call by taking in a changeable `to` argument that affects the future states. /// /// Petgraph's counterpart of this trait is its /// [`petgraph::visit::Walker`] trait. @@ -53,9 +53,25 @@ pub trait Abort { fn abort(&mut self, context: &mut C); } -/// Steppers that may receive discrete events and act on them, implement this trait. +/// Steppers that can receive discrete events and act on them, implement this +/// trait. +// XXX: Doesn't this violate the rule that stepper's future states are +// determined by its initial state? pub trait OnEvent { type Output; fn on_event(&mut self, context: &mut Ctx, event: Event) -> Self::Output; } + +/// Some steppers report estimates of how far they are from completion. +pub trait EstimateProgress { + type Value: Default; + + fn estimate_progress_value(&self) -> Self::Value { + Self::Value::default() + } + + fn estimate_progress_maximum(&self) -> Self::Value { + Self::Value::default() + } +}