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.73: neck-down, length tuning, grounded coplanar (#52)
2a6bec0
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
//! 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));
}
}