app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
main
Colby Knox
AI Flow 0.2.67: signal and placement rankings (#129 #130)
a34ceb8
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
//! Issue #129: the AI's ranking of this board's signals, `spec.signalPriority`, an ordered list of classes:
//!
//! ```json
//! [{"class": "power loop", "nets": ["VBAT", "PH*", "GND"], "why": "20 A, sets heat and EMI"},
//! {"class": "gate drive", "nets": ["GH*", "GL*"], "why": "highest di/dt", "viaBudget": 0, "maxLengthMm": 25},
//! {"class": "BEMF sense", "nets": ["BEMF_*"], "why": "analogue zero-crossing", "sensitive": true}]
//! ```
//!
//! First is most important. `nets` are names or patterns (`*` any run, `?` one character; case is ignored,
//! and a hierarchical `/GND` matches `GND`). The router routes the classes in this order; `place check` and
//! the route-quality report measure each class against its preferences. Preferences are compared, never
//! enforced: AI Flow suggests, it does not refuse.
use serde_json::Value;
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Class {
pub class: String,
pub nets: Vec<String>,
pub why: String,
/// vias the class should need at most per net (a preference the report compares against)
pub via_budget: Option<usize>,
/// a routed net of the class longer than this is reported
pub max_length_mm: Option<f64>,
/// a class that suffers from noise (analogue sense, a receiver input): `place check` measures its distance to noisy nets
pub sensitive: bool,
/// a class that makes noise (a switch node, gate drive): the other side of that distance
pub noisy: bool,
}
/// The ranking in the spec, first is most important. A class without a name or nets is left out.
pub fn from_spec(spec: &Value) -> Vec<Class> {
spec["signalPriority"].as_array().into_iter().flatten().filter_map(|c| {
let nets: Vec<String> = match &c["nets"] {
Value::Array(a) => a.iter().filter_map(|n| n.as_str().map(str::to_string)).collect(),
Value::String(s) => vec![s.clone()],
_ => Vec::new(),
};
let class = c["class"].as_str().or(c["name"].as_str())?.to_string();
(!nets.is_empty()).then(|| Class {
class, nets, why: c["why"].as_str().unwrap_or("").to_string(),
via_budget: c["viaBudget"].as_u64().map(|v| v as usize),
max_length_mm: c["maxLengthMm"].as_f64(),
sensitive: c["sensitive"].as_bool().unwrap_or(false),
noisy: c["noisy"].as_bool().unwrap_or(false),
})
}).collect()
}
fn glob_ci(pat: &[char], s: &[char]) -> bool {
match (pat.first(), s.first()) {
(None, None) => true,
(Some('*'), _) => glob_ci(&pat[1..], s) || (!s.is_empty() && glob_ci(pat, &s[1..])),
(Some('?'), Some(_)) => glob_ci(&pat[1..], &s[1..]),
(Some(p), Some(c)) if p.to_lowercase().eq(c.to_lowercase()) => glob_ci(&pat[1..], &s[1..]),
_ => false,
}
}
/// Does a net name match a pattern: the whole name, the name without a leading `/`, or its last path part.
pub fn matches(pattern: &str, net: &str) -> bool {
let p: Vec<char> = pattern.trim().trim_start_matches('/').chars().collect();
let trimmed = net.trim_start_matches('/');
[trimmed, trimmed.rsplit('/').next().unwrap_or(trimmed)].iter().any(|n| glob_ci(&p, &n.chars().collect::<Vec<_>>()))
}
/// The rank (0 = most important) of the first class whose patterns match the net.
pub fn rank_of(classes: &[Class], net: &str) -> Option<usize> {
classes.iter().position(|c| c.nets.iter().any(|p| matches(p, net)))
}
/// Put the items in class order, most important class first, the unranked last; a stable sort, so the
/// order within a class (the router's own, or a variant's) stands.
pub fn sort_by_rank<T>(items: &mut [T], classes: &[Class], name: impl Fn(&T) -> String) {
if classes.is_empty() { return; }
items.sort_by_key(|i| rank_of(classes, &name(i)).unwrap_or(usize::MAX));
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn classes_read_in_order_and_patterns_match_like_a_designer_writes_them() {
let spec = json!({"signalPriority": [
{"class": "gate drive", "nets": ["GH*", "GL?"], "why": "di/dt", "viaBudget": 0, "noisy": true},
{"class": "sense", "nets": "BEMF_*", "sensitive": true},
{"class": "empty", "nets": []},
{"nets": ["X"]}]});
let c = from_spec(&spec);
assert_eq!(c.len(), 2);
assert_eq!((c[0].via_budget, c[0].noisy, c[1].sensitive), (Some(0), true, true));
assert_eq!(rank_of(&c, "/GHA"), Some(0));
assert_eq!(rank_of(&c, "/drive/GLB"), Some(0), "the last part of a hierarchical name");
assert_eq!(rank_of(&c, "gla"), Some(0), "case is ignored");
assert_eq!(rank_of(&c, "GLAB"), None, "? is one character");
assert_eq!(rank_of(&c, "BEMF_A"), Some(1));
assert_eq!(rank_of(&c, "SWD"), None);
// the router's order (shortest first, say) stands inside a class; the ranking decides between classes
let mut nets = vec!["SWD", "BEMF_A", "GHB", "TLM", "GHA", "BEMF_B"];
sort_by_rank(&mut nets, &c, |n| n.to_string());
assert_eq!(nets, vec!["GHB", "GHA", "BEMF_A", "BEMF_B", "SWD", "TLM"]);
let mut same = vec!["B", "A"];
sort_by_rank(&mut same, &[], |n| n.to_string());
assert_eq!(same, vec!["B", "A"], "no ranking: nothing moves");
}
}