Compare commits

...

2 Commits

Author SHA1 Message Date
Mikolaj Wielgus f2c67ed81d fix(autorouter/planar_reconfigurer): Differentiate search nodes also by length
This fixes 4x4_1206_led_matrix_breakout test.
2025-11-18 19:05:22 +01:00
Mikolaj Wielgus 88f8b3610d refactor(autorouter/planar_reconfigurer): Use generic A* to find planar configurations
This makes the 4x4_1206_led_matrix_breakout test fail.

However, commenting out

```
if curr_g_score + *edge_g_cost >= entry_score {
    continue;
}
```

makes the test successful again. I will investigate this soon.
2025-11-18 18:42:34 +01:00
9 changed files with 301 additions and 167 deletions

View File

@ -312,7 +312,7 @@ impl<'a> Displayer<'a> {
.get(&navnode) .get(&navnode)
.map_or_else(String::new, |s| format!("g={:.2}", s)); .map_or_else(String::new, |s| format!("g={:.2}", s));
let estimate_score_text = thetastar let estimate_score_text = thetastar
.cost_to_goal_estimate_scores() .estimated_costs()
.get(&navnode) .get(&navnode)
.map_or_else(String::new, |s| format!("(f={:.2})", s)); .map_or_else(String::new, |s| format!("(f={:.2})", s));
let debug_text = activity.navnode_debug_text(navnode).unwrap_or(""); let debug_text = activity.navnode_debug_text(navnode).unwrap_or("");

66
src/astar.rs Normal file
View File

@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2025 Topola contributors
//
// SPDX-License-Identifier: MIT
use std::{
collections::{btree_map::Entry, BTreeMap, BinaryHeap},
ops::Add,
};
use derive_getters::Getters;
use crate::scored::MinScored;
#[derive(Getters)]
pub struct Astar<N, S> {
#[getter(skip)]
frontier: BinaryHeap<MinScored<S, N>>,
#[getter(skip)]
g_scores: BTreeMap<N, S>,
curr_node: N,
}
impl<N: Clone + Ord, S: Add<S, Output = S> + Copy + Default + PartialOrd> Astar<N, S> {
pub fn new(start: N) -> Self {
let mut frontier = BinaryHeap::new();
let mut scores = BTreeMap::new();
scores.insert(start.clone(), S::default());
frontier.push(MinScored(S::default(), start.clone()));
Self {
frontier,
g_scores: scores,
curr_node: start,
}
}
pub fn expand(&mut self, new_nodes: &[(S, S, N)]) -> Option<N> {
let curr_g_score = self.g_scores.get(&self.curr_node).unwrap().clone();
for (edge_g_cost, h_heuristic, node) in new_nodes {
match self.g_scores.entry(node.clone()) {
Entry::Occupied(mut entry) => {
let entry_score = *entry.get();
if curr_g_score + *edge_g_cost >= entry_score {
continue;
}
entry.insert(curr_g_score + *edge_g_cost);
}
Entry::Vacant(entry) => {
entry.insert(curr_g_score + *edge_g_cost);
}
}
self.frontier.push(MinScored(
curr_g_score + *edge_g_cost + *h_heuristic,
node.clone(),
));
}
MinScored(_ /*f_score*/, self.curr_node) = self.frontier.pop()?;
Some(self.curr_node.clone())
}
}

View File

@ -14,7 +14,6 @@ pub mod multilayer_autoroute;
pub mod multilayer_preconfigurer; pub mod multilayer_preconfigurer;
pub mod multilayer_reconfigurator; pub mod multilayer_reconfigurator;
pub mod multilayer_reconfigurer; pub mod multilayer_reconfigurer;
pub mod permsearch;
pub mod place_via; pub mod place_via;
pub mod planar_autoroute; pub mod planar_autoroute;
pub mod planar_preconfigurer; pub mod planar_preconfigurer;

View File

@ -1,106 +0,0 @@
// SPDX-FileCopyrightText: 2025 Topola contributors
//
// SPDX-License-Identifier: MIT
use std::{cmp::Ordering, collections::BinaryHeap, iter::Skip, iter::Take};
use itertools::{Itertools, Permutations};
#[derive(Clone, Debug)]
struct PermsearchNode<T> {
curr_permutation: Vec<T>,
permutations: Skip<Permutations<Take<std::vec::IntoIter<T>>>>,
length: usize,
}
impl<T: Eq> PartialEq for PermsearchNode<T> {
fn eq(&self, other: &Self) -> bool {
self.curr_permutation == other.curr_permutation
}
}
impl<T: Eq> Eq for PermsearchNode<T> {}
impl<T: Eq> Ord for PermsearchNode<T> {
fn cmp(&self, other: &Self) -> Ordering {
self.length.cmp(&other.length)
}
}
impl<T: Eq> PartialOrd for PermsearchNode<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.length.partial_cmp(&other.length)
}
}
impl<T: Clone> PermsearchNode<T> {
fn permute(mut self) -> Option<Self> {
for (i, element) in self.permutations.next()?.iter().enumerate() {
self.curr_permutation[i] = element.clone();
}
Some(self)
}
fn resize(self, length: usize) -> Option<Self> {
if length == self.length {
return None;
}
// TODO: Get rid of `self.curr_permutation` clone somehow?
Some(Self {
curr_permutation: self.curr_permutation.clone(),
permutations: self
.curr_permutation
.into_iter()
.take(length)
.permutations(length)
.skip(1),
length,
})
}
}
pub struct Permsearch<T> {
curr_node: PermsearchNode<T>,
frontier: BinaryHeap<PermsearchNode<T>>,
}
impl<T: Eq + Clone> Permsearch<T> {
pub fn new(original: Vec<T>) -> Self {
let len = original.len();
// TODO: Get rid of `original` clone somehow?
Self {
curr_node: PermsearchNode {
curr_permutation: original.clone(),
permutations: original.into_iter().take(len).permutations(0).skip(0),
length: 0,
},
frontier: BinaryHeap::new(),
}
}
pub fn step(&mut self, len: usize) -> Option<&[T]> {
// TODO: Get rid of `self.curr_node` clones somehow?
if let Some(resized_curr_node) = self.curr_node.clone().resize(len) {
if let Some(permuted_resized_curr_node) = resized_curr_node.permute() {
self.frontier.push(permuted_resized_curr_node);
}
}
if let Some(permuted_curr_node) = self.curr_node.clone().permute() {
self.frontier.push(permuted_curr_node);
}
self.curr_node = self.frontier.pop()?;
Some(&self.curr_node.curr_permutation)
}
pub fn curr_permutation(&self) -> &[T] {
&self.curr_node.curr_permutation
}
}

View File

@ -2,15 +2,24 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use std::{
cmp::Ordering,
iter::{Skip, Take},
};
use derive_getters::Getters;
use enum_dispatch::enum_dispatch; use enum_dispatch::enum_dispatch;
use itertools::{Itertools, Permutations};
use specctra_core::mesadata::AccessMesadata; use specctra_core::mesadata::AccessMesadata;
use crate::autorouter::{ use crate::{
permsearch::Permsearch, astar::Astar,
planar_autoroute::{PlanarAutorouteConfiguration, PlanarAutorouteExecutionStepper}, autorouter::{
planar_preconfigurer::SccIntersectionsAndLengthRatlinePlanarAutoroutePreconfigurer, planar_autoroute::{PlanarAutorouteConfiguration, PlanarAutorouteExecutionStepper},
scc::Scc, planar_preconfigurer::SccIntersectionsAndLengthRatlinePlanarAutoroutePreconfigurer,
Autorouter, PlanarAutorouteOptions, scc::Scc,
Autorouter, PlanarAutorouteOptions,
},
}; };
#[enum_dispatch] #[enum_dispatch]
@ -49,8 +58,100 @@ impl PlanarAutorouteReconfigurer {
} }
} }
#[derive(Clone, Debug, Getters)]
struct SccSearchNode {
curr_permutation: Vec<Scc>,
#[getter(skip)]
permutations: Skip<Permutations<Take<std::vec::IntoIter<Scc>>>>,
#[getter(skip)]
length: usize,
}
impl Ord for SccSearchNode {
fn cmp(&self, other: &Self) -> Ordering {
self.curr_permutation
.cmp(&other.curr_permutation)
.then(self.length.cmp(&other.length))
}
}
impl PartialOrd for SccSearchNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Eq for SccSearchNode {}
impl PartialEq for SccSearchNode {
fn eq(&self, other: &Self) -> bool {
self.curr_permutation == other.curr_permutation && self.length == other.length
}
}
impl SccSearchNode {
pub fn new(sccs: Vec<Scc>) -> Self {
let len = sccs.len();
Self {
curr_permutation: sccs.clone(),
permutations: sccs.into_iter().take(len).permutations(0).skip(0),
length: 0,
}
}
pub fn expand(&self, length: usize) -> Vec<(f64, f64, Self)> {
let mut expanded_nodes = vec![];
if let Some(resized) = self.clone().resize(length) {
if let Some(permuted_resized) = resized.permute() {
expanded_nodes.push((
0.1,
(self.curr_permutation.len() - length) as f64,
permuted_resized,
));
}
}
if let Some(permuted) = self.clone().permute() {
expanded_nodes.push((
0.1,
(self.curr_permutation.len() - self.length) as f64,
permuted,
));
}
expanded_nodes
}
fn resize(self, length: usize) -> Option<Self> {
if length == self.length {
return None;
}
Some(Self {
curr_permutation: self.curr_permutation.clone(),
permutations: self
.curr_permutation
.into_iter()
.take(length)
.permutations(length)
.skip(1),
length,
})
}
fn permute(mut self) -> Option<Self> {
for (i, element) in self.permutations.next()?.iter().enumerate() {
self.curr_permutation[i] = element.clone();
}
Some(self)
}
}
pub struct SccPermutationsPlanarAutorouteReconfigurer { pub struct SccPermutationsPlanarAutorouteReconfigurer {
sccs_permsearch: Permsearch<Scc>, sccs_search: Astar<SccSearchNode, f64>,
preconfiguration: PlanarAutorouteConfiguration, preconfiguration: PlanarAutorouteConfiguration,
} }
@ -66,7 +167,7 @@ impl SccPermutationsPlanarAutorouteReconfigurer {
let sccs = presorter.dissolve(); let sccs = presorter.dissolve();
Self { Self {
sccs_permsearch: Permsearch::new(sccs), sccs_search: Astar::new(SccSearchNode::new(sccs)),
preconfiguration, preconfiguration,
} }
} }
@ -79,7 +180,8 @@ impl MakeNextPlanarAutorouteConfiguration for SccPermutationsPlanarAutorouteReco
stepper: &PlanarAutorouteExecutionStepper, stepper: &PlanarAutorouteExecutionStepper,
) -> Option<PlanarAutorouteConfiguration> { ) -> Option<PlanarAutorouteConfiguration> {
let scc_index = self let scc_index = self
.sccs_permsearch .sccs_search
.curr_node()
.curr_permutation() .curr_permutation()
.iter() .iter()
.position(|scc| { .position(|scc| {
@ -88,10 +190,13 @@ impl MakeNextPlanarAutorouteConfiguration for SccPermutationsPlanarAutorouteReco
}) })
.unwrap(); .unwrap();
let scc_permutation = self.sccs_permsearch.step(scc_index + 1)?; let next_search_node = self
.sccs_search
.expand(&self.sccs_search.curr_node().expand(scc_index + 1))?;
let next_permutation = next_search_node.curr_permutation();
let mut ratlines = vec![]; let mut ratlines = vec![];
for scc in scc_permutation { for scc in next_permutation {
for ratline in self.preconfiguration.ratlines.iter() { for ratline in self.preconfiguration.ratlines.iter() {
if scc.node_indices().contains( if scc.node_indices().contains(
&autorouter &autorouter

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use std::collections::BTreeSet; use std::{cmp::Ordering, collections::BTreeSet};
use derive_getters::Getters; use derive_getters::Getters;
use petgraph::{graph::NodeIndex, prelude::StableUnGraph}; use petgraph::{graph::NodeIndex, prelude::StableUnGraph};
@ -26,6 +26,18 @@ pub struct Scc {
length: f64, length: f64,
} }
impl Ord for Scc {
fn cmp(&self, other: &Self) -> Ordering {
self.node_indices.cmp(&other.node_indices)
}
}
impl PartialOrd for Scc {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Scc { impl PartialEq for Scc {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.node_indices == other.node_indices self.node_indices == other.node_indices

View File

@ -21,6 +21,7 @@
pub mod graph; pub mod graph;
#[macro_use] #[macro_use]
pub mod drawing; pub mod drawing;
pub mod astar;
pub mod autorouter; pub mod autorouter;
pub mod bimapset; pub mod bimapset;
pub mod board; pub mod board;
@ -29,6 +30,7 @@ pub mod interactor;
pub mod layout; pub mod layout;
pub mod math; pub mod math;
pub mod router; pub mod router;
pub mod scored;
pub mod specctra; pub mod specctra;
pub mod stepper; pub mod stepper;
pub mod triangulation; pub mod triangulation;

View File

@ -18,52 +18,9 @@ use petgraph::algo::Measure;
use petgraph::visit::{EdgeRef, GraphBase, IntoEdgeReferences, IntoEdges}; use petgraph::visit::{EdgeRef, GraphBase, IntoEdgeReferences, IntoEdges};
use thiserror::Error; use thiserror::Error;
use std::cmp::Ordering; use crate::scored::MinScored;
use crate::stepper::{EstimateProgress, LinearScale, Step}; use crate::stepper::{EstimateProgress, LinearScale, Step};
#[derive(Copy, Clone, Debug)]
pub struct MinScored<K, T>(pub K, pub T);
impl<K: PartialOrd, T> PartialEq for MinScored<K, T> {
#[inline]
fn eq(&self, other: &MinScored<K, T>) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl<K: PartialOrd, T> Eq for MinScored<K, T> {}
impl<K: PartialOrd, T> PartialOrd for MinScored<K, T> {
#[inline]
fn partial_cmp(&self, other: &MinScored<K, T>) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<K: PartialOrd, T> Ord for MinScored<K, T> {
#[inline]
fn cmp(&self, other: &MinScored<K, T>) -> Ordering {
let a = &self.0;
let b = &other.0;
if a == b {
Ordering::Equal
} else if a < b {
Ordering::Greater
} else if a > b {
Ordering::Less
} else if a.ne(a) && b.ne(b) {
// these are the NaN cases
Ordering::Equal
} else if a.ne(a) {
// Order NaN less, so that it is last in the MinScore order
Ordering::Less
} else {
Ordering::Greater
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct PathTracker<G> pub struct PathTracker<G>
where where
@ -179,7 +136,7 @@ where
/// Also known as the g-scores, or just g. /// Also known as the g-scores, or just g.
scores: BTreeMap<G::NodeId, K>, scores: BTreeMap<G::NodeId, K>,
/// Also known as the f-scores, or just f. /// Also known as the f-scores, or just f.
cost_to_goal_estimate_scores: BTreeMap<G::NodeId, K>, estimated_costs: BTreeMap<G::NodeId, K>,
#[getter(skip)] #[getter(skip)]
path_tracker: PathTracker<G>, path_tracker: PathTracker<G>,
// FIXME: To work around edge references borrowing from the graph we collect then reiterate over them. // FIXME: To work around edge references borrowing from the graph we collect then reiterate over them.
@ -218,7 +175,7 @@ where
graph, graph,
frontier: BinaryHeap::new(), frontier: BinaryHeap::new(),
scores: BTreeMap::new(), scores: BTreeMap::new(),
cost_to_goal_estimate_scores: BTreeMap::new(), estimated_costs: BTreeMap::new(),
path_tracker: PathTracker::<G>::new(), path_tracker: PathTracker::<G>::new(),
edge_ids: Vec::new(), edge_ids: Vec::new(),
progress_estimate_value: K::default(), progress_estimate_value: K::default(),
@ -288,7 +245,7 @@ where
return Ok(ControlFlow::Break((cost, path, result))); return Ok(ControlFlow::Break((cost, path, result)));
} }
match self.cost_to_goal_estimate_scores.entry(navnode) { match self.estimated_costs.entry(navnode) {
Entry::Occupied(mut entry) => { Entry::Occupied(mut entry) => {
// If the node has already been visited with an equal or lower // 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. // estimated score than now, then we do not need to re-visit it.

99
src/scored.rs Normal file
View File

@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: (None)
//
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copied verbatim from petgraph-0.8.3.
use core::cmp::Ordering;
/// `MinScored<K, T>` holds a score `K` and a scored object `T` in
/// a pair for use with a `BinaryHeap`.
///
/// `MinScored` compares in reverse order by the score, so that we can
/// use `BinaryHeap` as a min-heap to extract the score-value pair with the
/// least score.
///
/// **Note:** `MinScored` implements a total order (`Ord`), so that it is
/// possible to use float types as scores.
#[derive(Copy, Clone, Debug)]
pub struct MinScored<K, T>(pub K, pub T);
impl<K: PartialOrd, T> PartialEq for MinScored<K, T> {
#[inline]
fn eq(&self, other: &MinScored<K, T>) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl<K: PartialOrd, T> Eq for MinScored<K, T> {}
impl<K: PartialOrd, T> PartialOrd for MinScored<K, T> {
#[inline]
fn partial_cmp(&self, other: &MinScored<K, T>) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<K: PartialOrd, T> Ord for MinScored<K, T> {
#[inline]
fn cmp(&self, other: &MinScored<K, T>) -> Ordering {
let a = &self.0;
let b = &other.0;
if a == b {
Ordering::Equal
} else if a < b {
Ordering::Greater
} else if a > b {
Ordering::Less
} else if a.ne(a) && b.ne(b) {
// these are the NaN cases
Ordering::Equal
} else if a.ne(a) {
// Order NaN less, so that it is last in the MinScore order
Ordering::Less
} else {
Ordering::Greater
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct MaxScored<K, T>(pub K, pub T);
impl<K: PartialOrd, T> PartialEq for MaxScored<K, T> {
#[inline]
fn eq(&self, other: &MaxScored<K, T>) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl<K: PartialOrd, T> Eq for MaxScored<K, T> {}
impl<K: PartialOrd, T> PartialOrd for MaxScored<K, T> {
#[inline]
fn partial_cmp(&self, other: &MaxScored<K, T>) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<K: PartialOrd, T> Ord for MaxScored<K, T> {
#[inline]
fn cmp(&self, other: &MaxScored<K, T>) -> Ordering {
let a = &self.0;
let b = &other.0;
if a == b {
Ordering::Equal
} else if a < b {
Ordering::Less
} else if a > b {
Ordering::Greater
} else if a.ne(a) && b.ne(b) {
// these are the NaN cases
Ordering::Equal
} else if a.ne(a) {
Ordering::Less
} else {
Ordering::Greater
}
}
}