main
Colby Knox AI Flow 0.2.73: neck-down, length tuning, grounded coplanar (#52) 2a6bec0
//! The board rules the router works to. The defaults are KiCad's for a 4-layer board with 0.2 mm
//! clearance; the spec's config overrides them (wide nets, planes, Kelvin pairs, via sizes).
use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Rules {
    pub track: f64,
    pub clearance: f64,
    /// Per-net clearance overrides (spec "netClearance": {net: mm}), for a net the DRC caught a hair under the rule.
    pub net_clearance: std::collections::BTreeMap<String, f64>,
    pub via: f64,
    pub drill: f64,
    pub via_small: f64,
    pub drill_small: f64,
    pub edge: f64,
    pub stub: f64,
    pub narrow: f64,
    pub escape: f64,
    pub corridor: f64,
    pub ic_zone: f64,
    pub channel: f64,
    pub planes: BTreeMap<String, String>,
    pub wide: BTreeMap<String, Vec<f64>>,
    pub mid: BTreeMap<String, Vec<f64>>,
    pub hole_to_hole: f64,
    pub via_search_mm: f64,
    pub kelvin: BTreeMap<String, String>,
    /// spec.signalPriority (issue #129): the AI's ranking of the signal classes, routed in this order
    #[serde(default)]
    pub signal_priority: Vec<crate::signal_priority::Class>,
    /// Layer policy (spec "planeUnder"): regions of a layer that belong to one net's plane; every
    /// other net treats them as a keepout on that layer (vias included, they pierce both layers).
    pub plane_under: Vec<PlaneUnder>,
    /// Assigned-pin decoupling pairs (spec "decoupling"), routed before the rail (decoupling.rs).
    pub decoupling: Vec<crate::decoupling::Pair>,
    /// Impedance-controlled nets (issue #52): the width from the impedance table, the one layer the
    /// net is routed on (no vias), and for a pair its partner and centre pitch. Filled by `route`
    /// from impedance::table, never from the spec directly.
    pub impedance: BTreeMap<String, ImpedanceRoute>,
    /// Issue #183: the copper layers the router may use (spec "routeLayers", or the project's
    /// `.kicad_dru` `disallow track` on a layer). Empty means both outer layers (the default).
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub route_layers: Vec<String>,
    /// Issue #183: false forbids every via (spec "vias": false, or the project's `.kicad_dru`
    /// `(constraint disallow via)`). A layer change is then made only at a through-hole pad of the
    /// same net, where the component lead joins both faces.
    #[serde(skip_serializing_if = "is_true")]
    pub vias: bool,
}

fn is_true(b: &bool) -> bool { *b }

/// How the router routes one impedance-controlled net.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ImpedanceRoute {
    pub target: String,
    pub width: f64,
    pub layer: String,
    pub partner: Option<String>,
    pub pitch: Option<f64>,
    /// a pair's length mismatch budget (spec maxSkewMm, default 0.5): neckdown::tune_pairs meanders the shorter net
    pub max_skew: f64,
}

/// One `planeUnder` entry: `{"layer": "B.Cu", "net": "GND", "around": ["U1", "C2.1"], "margin": 1.0}`
/// (the pad box of the listed parts or pads grown by the margin) or an explicit `"polygon"`.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct PlaneUnder {
    pub layer: String,
    pub net: String,
    pub around: Vec<String>,
    pub margin: Option<f64>,
    pub polygon: Vec<(f64, f64)>,
    pub name: Option<String>,
}

impl Default for Rules {
    fn default() -> Rules {
        Rules {
            track: 0.25, clearance: 0.2, net_clearance: Default::default(), via: 0.8, drill: 0.4, via_small: 0.6, drill_small: 0.3, edge: 0.5,
            stub: 0.25, narrow: 0.5, escape: 0.45, corridor: 0.5, ic_zone: 2.0, channel: 2.5,
            planes: BTreeMap::new(), wide: BTreeMap::new(), mid: BTreeMap::new(), hole_to_hole: 0.25, via_search_mm: 6.0, kelvin: BTreeMap::new(), signal_priority: Vec::new(), plane_under: Vec::new(), decoupling: Vec::new(), impedance: BTreeMap::new(),
            route_layers: Vec::new(), vias: true,
        }
    }
}

impl Rules {
    /// The router rules from a aiflow spec (planes, wideNets, midNets, kelvin, clearance, copper sizes).
    pub fn from_spec(spec: &serde_json::Value) -> Rules {
        let mut r = Rules::default();
        let get_map = |k: &str| -> BTreeMap<String, String> {
            spec.get(k).and_then(|v| v.as_object()).map(|o| o.iter().filter_map(|(a, b)| Some((a.clone(), b.as_str()?.to_string()))).collect()).unwrap_or_default()
        };
        let get_widths = |k: &str| -> BTreeMap<String, Vec<f64>> {
            // a list of widths, or one width as a number (issue #165: a scalar was dropped without a word)
            spec.get(k).and_then(|v| v.as_object()).map(|o| o.iter().filter_map(|(a, b)| {
                let w: Vec<f64> = match b { serde_json::Value::Array(x) => x.iter().filter_map(|x| x.as_f64()).collect(), _ => vec![b.as_f64()?] };
                Some((a.clone(), w))
            }).collect()).unwrap_or_default()
        };
        r.planes = get_map("planes");
        r.kelvin = get_map("kelvin");
        r.wide = get_widths("wideNets");
        r.mid = get_widths("midNets");
        r.plane_under = PlaneUnder::from_spec(spec);
        r.signal_priority = crate::signal_priority::from_spec(spec);
        r.decoupling = crate::decoupling::Pair::from_spec(spec).unwrap_or_default();
        let num = |k: &str| spec.get(k).and_then(|v| v.as_f64());
        if let Some(v) = num("clearance") { r.clearance = v; }
        if let Some(m) = spec.get("netClearance").and_then(|v| v.as_object()) {
            for (k, v) in m { if let Some(x) = v.as_f64() { r.net_clearance.insert(k.clone(), x); } }
        }
        if let Some(v) = num("edgeClearance") { r.edge = v; }
        if let Some(v) = num("holeToHoleClearance").or_else(||num("holeToHole")) { r.hole_to_hole = v; }
        // `trackWidth` is read as `track` (issue #165), `track` wins when both are there
        if let Some(v) = num("track").or_else(|| num("trackWidth")) { r.track = v; }
        // Escape necks inherit the requested track width. An explicit smaller
        // stub must still satisfy the separately declared manufacturing minimum.
        r.stub = num("stub").unwrap_or(r.track).max(num("minTrackWidth").unwrap_or(r.track));
        // a via is a size, or a [size, drill] pair (the way wideNets gives widths); either form is read,
        // never silently skipped
        let pair = |k: &str| spec.get(k).and_then(|v| v.as_array()).and_then(|a| Some((a.first()?.as_f64()?, a.get(1).and_then(|d| d.as_f64()))));
        if let Some(v) = num("viaSignal") { r.via_small = v; }
        if let Some((v, d)) = pair("viaSignal") { r.via_small = v; if let Some(d) = d { r.drill_small = d; } }
        if let Some(v) = num("drillSignal") { r.drill_small = v; }
        if let Some(v) = num("viaPower") { r.via = v; }
        if let Some((v, d)) = pair("viaPower") { r.via = v; if let Some(d) = d { r.drill = d; } }
        if let Some(v) = num("drillPower") { r.drill = v; }
        // issue #183: single-face / no-via boards (main.rs also reads the project's .kicad_dru)
        r.route_layers = spec_route_layers(spec).unwrap_or_default();
        if let Some(v) = spec.get("vias").and_then(|v| v.as_bool()) { r.vias = v; }
        r
    }

    /// May the router put copper on this layer (F_CU or B_CU)?
    pub fn layer_allowed(&self, l: usize) -> bool {
        self.route_layers.is_empty() || self.route_layers.iter().any(|n| n == aiflow_board::LAYER_NAMES[l])
    }

    /// Both outer layers and vias: today's two-layer router.
    pub fn default_copper_mode(&self) -> bool {
        self.vias && self.layer_allowed(aiflow_board::F_CU) && self.layer_allowed(aiflow_board::B_CU)
    }

    /// May the router change layer anywhere (a via)? Never with one layer, never with vias off.
    pub fn vias_allowed(&self) -> bool {
        self.default_copper_mode()
    }

    /// With both layers and no vias, F.Cu copper is a wire jumper on the component side and B.Cu is
    /// the milled (primary) face: the layer the router charges extra to enter.
    pub fn jumper_layer(&self) -> Option<usize> {
        (!self.vias && self.layer_allowed(aiflow_board::F_CU) && self.layer_allowed(aiflow_board::B_CU)).then_some(aiflow_board::F_CU)
    }

    /// The face a single-face board is routed on: the one allowed layer, or B.Cu when F.Cu is jumpers.
    pub fn primary_layer(&self) -> usize {
        if !self.layer_allowed(aiflow_board::B_CU) { aiflow_board::F_CU } else { aiflow_board::B_CU }
    }
}

/// The spec's "routeLayers" as layer names the router knows (F.Cu, B.Cu), in board order; None when
/// the spec has no such key or names no layer the router knows.
pub fn spec_route_layers(spec: &serde_json::Value) -> Option<Vec<String>> {
    let a = spec.get("routeLayers")?.as_array()?;
    let names: Vec<String> = ["F.Cu", "B.Cu"].iter().filter(|l| a.iter().any(|v| v.as_str() == Some(**l))).map(|l| l.to_string()).collect();
    if names.is_empty() { None } else if names.len() == 2 { Some(Vec::new()) } else { Some(names) }
}

impl PlaneUnder {
    /// Every `planeUnder` entry of a spec (an array, or one object); an entry that does not parse is skipped.
    pub fn from_spec(spec: &serde_json::Value) -> Vec<PlaneUnder> {
        let v = spec.get("planeUnder").cloned().unwrap_or(serde_json::Value::Null);
        let items = match v { serde_json::Value::Array(a) => a, serde_json::Value::Object(_) => vec![v], _ => Vec::new() };
        items.into_iter().filter_map(|e| serde_json::from_value(e).ok()).collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    #[test] fn escape_width_respects_manufacturing_contract() {
        assert_eq!(Rules::from_spec(&json!({"track":0.4})).stub,0.4);
        // issue #165: trackWidth is track; scalar widths are one-entry lists
        let r = Rules::from_spec(&json!({"trackWidth":0.15,"wideNets":{"VBAT":0.5,"GND":[0.6,0.4]},"midNets":{"SW":0.3}}));
        assert_eq!(r.track, 0.15);
        assert_eq!(r.wide["VBAT"], vec![0.5]);
        assert_eq!(r.wide["GND"], vec![0.6, 0.4]);
        assert_eq!(r.mid["SW"], vec![0.3]);
        assert_eq!(Rules::from_spec(&json!({"track":0.2,"trackWidth":0.15})).track, 0.2);
        assert_eq!(Rules::from_spec(&json!({"track":0.4,"stub":0.1})).stub,0.4);
        assert_eq!(Rules::from_spec(&json!({"track":0.4,"stub":0.2,"minTrackWidth":0.15})).stub,0.2);
    }
    #[test] fn plane_under_reads_an_array_or_one_object() {
        let r = Rules::from_spec(&json!({"planeUnder":[{"layer":"B.Cu","net":"GND","around":["U1","C2"],"margin":1.0},{"layer":"B.Cu","net":"GND","polygon":[[0,0],[1,0],[1,1]]}]}));
        assert_eq!(r.plane_under.len(), 2);
        assert_eq!(r.plane_under[0].around, vec!["U1".to_string(), "C2".to_string()]);
        assert_eq!(r.plane_under[1].polygon.len(), 3);
        assert_eq!(Rules::from_spec(&json!({"planeUnder":{"layer":"F.Cu","net":"GND","around":["U1"]}})).plane_under.len(), 1);
        assert!(Rules::from_spec(&json!({})).plane_under.is_empty());
    }
}

#[cfg(test)]
mod via_pair_tests {
    #[test]
    fn via_power_as_pair_or_number() {
        let a = super::Rules::from_spec(&serde_json::json!({"viaPower": [1.0, 0.4]}));
        assert_eq!((a.via, a.drill), (1.0, 0.4));
        let b = super::Rules::from_spec(&serde_json::json!({"viaPower": 0.9, "drillPower": 0.35}));
        assert_eq!((b.via, b.drill), (0.9, 0.35));
    }
}