main
Colby Knox AI Flow 0.2.67: signal and placement rankings (#129 #130) a34ceb8
//! 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");
    }
}