use crate::parser::schema::{Schematic, Component, Position};
use crate::parser::pcb_schema::{PcbDesign, Footprint, Pad, Via, Zone, Position3D};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ICRiskScore {
pub ic_reference: String,
pub ic_value: String,
pub risk_index: f64, pub proximity_penalty: f64,
pub inductance_penalty: f64,
pub mismatch_penalty: f64,
pub net_criticality: NetCriticality,
pub decoupling_capacitors: Vec<CapacitorAnalysis>,
pub high_risk_heuristics: Vec<HighRiskHeuristic>,
pub location: Option<Position>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapacitorAnalysis {
pub capacitor_reference: String,
pub capacitor_value: String,
pub distance_mm: f64,
pub proximity_penalty: f64,
pub via_count: usize,
pub dog_bone_length_mm: f64,
pub inductance_penalty: f64,
pub inductance_nh: f64, pub capacitor_srf_mhz: f64,
pub ic_switching_freq_mhz: f64,
pub mismatch_penalty: f64,
pub shared_via: bool,
pub backside_offset: bool,
pub neck_down: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathAnalysis {
pub cap_ref: String,
pub ic_ref: String,
pub distance_mm: f64,
pub layer: String,
pub path_segments: Vec<PathSegment>,
pub net_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PathSegment {
Pad {
component_ref: String,
pad_number: String,
position: Position3D,
},
Trace {
uuid: String,
start: Position3D,
end: Position3D,
length_mm: f64,
layer: String,
},
Via {
uuid: String,
position: Position3D,
layers: (String, String),
},
Zone {
uuid: String,
layer: String,
entry_point: Position3D,
exit_point: Position3D,
distance_mm: f64,
},
}
#[derive(Debug, Clone)]
enum GraphNode {
Pad {
component_ref: String,
pad_number: String,
position: Position3D
},
TraceEnd {
uuid: String,
position: Position3D,
layer: String
},
ViaNode {
uuid: String,
position: Position3D,
#[allow(dead_code)]
layer: String
},
ZoneNode {
uuid: String,
position: Position3D,
layer: String
},
}
impl PartialEq for GraphNode {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(GraphNode::Pad { component_ref: r1, pad_number: n1, .. },
GraphNode::Pad { component_ref: r2, pad_number: n2, .. }) => {
r1 == r2 && n1 == n2
}
(GraphNode::TraceEnd { uuid: u1, .. }, GraphNode::TraceEnd { uuid: u2, .. }) => {
u1 == u2
}
(GraphNode::ViaNode { uuid: u1, .. }, GraphNode::ViaNode { uuid: u2, .. }) => {
u1 == u2
}
(GraphNode::ZoneNode { uuid: u1, .. }, GraphNode::ZoneNode { uuid: u2, .. }) => {
u1 == u2
}
_ => false,
}
}
}
impl Eq for GraphNode {}
impl std::hash::Hash for GraphNode {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
GraphNode::Pad { component_ref, pad_number, .. } => {
component_ref.hash(state);
pad_number.hash(state);
}
GraphNode::TraceEnd { uuid, .. } => {
uuid.hash(state);
}
GraphNode::ViaNode { uuid, .. } => {
uuid.hash(state);
}
GraphNode::ZoneNode { uuid, .. } => {
uuid.hash(state);
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PathError {
ComponentNotFound(String),
NetNotFound(String),
NoPathFound,
MultiplePathsFound,
InvalidNetConnection,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum NetCriticality {
Critical, High, Medium, Low, }
impl NetCriticality {
pub fn weight(&self) -> f64 {
match self {
NetCriticality::Critical => 1.0,
NetCriticality::High => 0.7,
NetCriticality::Medium => 0.5,
NetCriticality::Low => 0.2,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum HighRiskHeuristic {
SharedVia {
via_uuid: String,
capacitor1: String,
capacitor2: String,
},
BacksideOffset {
capacitor: String,
ic: String,
via_count: usize,
},
NeckDown {
capacitor: String,
trace_width_mm: f64,
plane_connection: bool,
},
}
pub struct DRSAnalyzer {
ic_switching_freqs: HashMap<String, f64>, capacitor_srf: HashMap<String, f64>, ic_max_inductance: HashMap<String, f64>, }
impl DRSAnalyzer {
pub fn new() -> Self {
let mut analyzer = Self {
ic_switching_freqs: HashMap::new(),
capacitor_srf: HashMap::new(),
ic_max_inductance: HashMap::new(),
};
analyzer.init_ic_frequencies();
analyzer.init_capacitor_srf();
analyzer.init_ic_inductance_limits();
analyzer
}
pub fn analyze(
&self,
schematic: &Schematic,
pcb: &PcbDesign,
) -> Vec<ICRiskScore> {
let mut results = Vec::new();
let ics: Vec<&Component> = schematic.components
.iter()
.filter(|c| {
let ref_upper = c.reference.to_uppercase();
ref_upper.starts_with('U')
})
.collect();
let ic_count = ics.len();
if ic_count == 0 {
tracing::warn!("No ICs found in schematic (components starting with 'U')");
return results;
}
let capacitors: Vec<&Component> = schematic.components
.iter()
.filter(|c| {
let ref_upper = c.reference.to_uppercase();
ref_upper.starts_with('C')
})
.collect();
let footprint_map = self.build_footprint_map(schematic, pcb);
let mut skipped_no_footprint = 0;
let mut skipped_no_power_pins = 0;
for ic in &ics {
if !footprint_map.contains_key(&ic.reference) {
tracing::debug!("Skipping IC {}: No footprint found on PCB", ic.reference);
skipped_no_footprint += 1;
continue;
}
if let Some(risk_score) = self.analyze_ic(
ic,
&capacitors,
schematic,
pcb,
&footprint_map,
) {
results.push(risk_score);
} else {
skipped_no_power_pins += 1;
tracing::debug!("Skipping IC {}: No power pins detected", ic.reference);
}
}
if results.is_empty() {
if skipped_no_footprint > 0 {
tracing::warn!("DRS analysis found {} ICs but none have footprints on PCB. Make sure the PCB file matches the schematic.", ic_count);
} else if skipped_no_power_pins > 0 {
tracing::warn!("DRS analysis found {} ICs with footprints but none have detectable power pins (VDD/VCC/VIN).", ic_count);
}
} else {
tracing::info!("DRS analysis: {} ICs analyzed, {} skipped (no footprint: {}, no power pins: {})",
results.len(), skipped_no_footprint + skipped_no_power_pins, skipped_no_footprint, skipped_no_power_pins);
}
results
}
fn analyze_ic(
&self,
ic: &Component,
capacitors: &[&Component],
schematic: &Schematic,
pcb: &PcbDesign,
footprint_map: &HashMap<String, &Footprint>,
) -> Option<ICRiskScore> {
let ic_footprint = footprint_map.get(&ic.reference)?;
let power_pins = self.find_power_pins(ic_footprint, schematic);
if power_pins.is_empty() {
return None;
}
let mut capacitor_analyses = Vec::new();
let mut high_risk_heuristics = Vec::new();
for capacitor in capacitors {
if let Some(cap_analysis) = self.analyze_capacitor(
capacitor,
ic,
ic_footprint,
&power_pins,
pcb,
footprint_map,
) {
if cap_analysis.shared_via {
high_risk_heuristics.push(HighRiskHeuristic::SharedVia {
via_uuid: "unknown".to_string(), capacitor1: capacitor.reference.clone(),
capacitor2: "unknown".to_string(), });
}
if cap_analysis.backside_offset {
high_risk_heuristics.push(HighRiskHeuristic::BacksideOffset {
capacitor: capacitor.reference.clone(),
ic: ic.reference.clone(),
via_count: cap_analysis.via_count,
});
}
if cap_analysis.neck_down {
high_risk_heuristics.push(HighRiskHeuristic::NeckDown {
capacitor: capacitor.reference.clone(),
trace_width_mm: 0.1, plane_connection: true,
});
}
capacitor_analyses.push(cap_analysis);
}
}
let net_criticality = self.classify_net_criticality(ic, schematic);
let (proximity_penalty, inductance_penalty, mismatch_penalty) =
self.calculate_penalties(&capacitor_analyses, &net_criticality);
let risk_index = (proximity_penalty + inductance_penalty + mismatch_penalty)
.min(100.0)
.max(0.0);
Some(ICRiskScore {
ic_reference: ic.reference.clone(),
ic_value: ic.value.clone(),
risk_index,
proximity_penalty,
inductance_penalty,
mismatch_penalty,
net_criticality,
decoupling_capacitors: capacitor_analyses,
high_risk_heuristics,
location: Some(ic.position.clone()),
})
}
fn analyze_capacitor(
&self,
capacitor: &Component,
ic: &Component,
ic_footprint: &Footprint,
power_pins: &[&Pad],
pcb: &PcbDesign,
footprint_map: &HashMap<String, &Footprint>,
) -> Option<CapacitorAnalysis> {
let cap_footprint = footprint_map.get(&capacitor.reference)?;
let cap_pads: Vec<&Pad> = cap_footprint.pads.iter().collect();
if cap_pads.len() < 2 {
return None;
}
let closest_power_pin = power_pins.iter()
.min_by(|a, b| {
let dist_a = self.distance_to_pad(&cap_pads[0].position, &a.position);
let dist_b = self.distance_to_pad(&cap_pads[0].position, &b.position);
dist_a.partial_cmp(&dist_b).unwrap_or(std::cmp::Ordering::Equal)
})?;
let distance_mm = self.distance_to_pad(&cap_pads[0].position, &closest_power_pin.position);
let proximity_penalty = if distance_mm <= 2.0 {
distance_mm * 2.0 } else {
4.0 + (distance_mm - 2.0).powf(2.0) * 5.0 };
let vias = self.find_connecting_vias(cap_footprint, pcb);
let via_count = vias.len();
let dog_bone_length_mm = if let Some(first_via) = vias.first() {
self.distance_to_pad(&cap_pads[0].position, &first_via.position)
} else {
0.0
};
let shared_via = self.check_shared_via(cap_footprint, pcb, footprint_map);
let backside_offset = cap_footprint.layer != ic_footprint.layer;
let inductance_penalty = if shared_via {
(via_count as f64 + dog_bone_length_mm) * 10.0 } else {
(via_count as f64 * 2.0) + (dog_bone_length_mm * 1.5)
};
let trace_inductance_nh = dog_bone_length_mm * 1.0;
let via_inductance_nh = via_count as f64 * 0.4;
let _total_inductance_nh = trace_inductance_nh + via_inductance_nh;
let capacitor_srf_mhz = self.get_capacitor_srf(&capacitor.value);
let ic_switching_freq_mhz = self.get_ic_switching_freq(&ic.value);
let mismatch_penalty = if capacitor_srf_mhz > 0.0 && ic_switching_freq_mhz > 0.0 {
let ratio = ic_switching_freq_mhz / capacitor_srf_mhz;
if ratio > 0.5 && ratio < 2.0 {
0.0 } else if ratio < 0.5 {
(0.5 - ratio) * 20.0 } else {
(ratio - 2.0) * 10.0 }
} else {
5.0 };
let neck_down = self.check_neck_down(cap_footprint, pcb);
let trace_inductance_nh = dog_bone_length_mm * 1.0; let via_inductance_nh = via_count as f64 * 0.4; let total_inductance_nh = trace_inductance_nh + via_inductance_nh;
Some(CapacitorAnalysis {
capacitor_reference: capacitor.reference.clone(),
capacitor_value: capacitor.value.clone(),
distance_mm,
proximity_penalty,
via_count,
dog_bone_length_mm,
inductance_penalty,
inductance_nh: total_inductance_nh,
capacitor_srf_mhz,
ic_switching_freq_mhz,
mismatch_penalty,
shared_via,
backside_offset,
neck_down,
})
}
fn calculate_penalties(
&self,
capacitor_analyses: &[CapacitorAnalysis],
net_criticality: &NetCriticality,
) -> (f64, f64, f64) {
let weight = net_criticality.weight();
let mut total_proximity = 0.0;
let mut total_inductance = 0.0;
let mut total_mismatch = 0.0;
for cap in capacitor_analyses {
total_proximity += cap.proximity_penalty;
total_inductance += cap.inductance_penalty;
total_mismatch += cap.mismatch_penalty;
}
let count = capacitor_analyses.len().max(1) as f64;
let avg_proximity = total_proximity / count;
let avg_inductance = total_inductance / count;
let avg_mismatch = total_mismatch / count;
let w_dist = 0.4; let w_ind = 0.4; let w_val = 0.2;
(
avg_proximity * w_dist * weight,
avg_inductance * w_ind * weight,
avg_mismatch * w_val * weight,
)
}
fn classify_net_criticality(&self, ic: &Component, schematic: &Schematic) -> NetCriticality {
let value_upper = ic.value.to_uppercase();
if value_upper.contains("STM32") ||
value_upper.contains("ESP32") ||
value_upper.contains("RP2040") ||
value_upper.contains("CPU") ||
value_upper.contains("MPU") {
for net in &schematic.nets {
let net_upper = net.name.to_uppercase();
if net_upper.contains("1.0V") || net_upper.contains("1.2V") || net_upper.contains("VDD_CORE") {
return NetCriticality::Critical;
}
}
return NetCriticality::High;
}
for net in &schematic.nets {
let net_upper = net.name.to_uppercase();
if net_upper.contains("1.0V") || net_upper.contains("1.2V") {
return NetCriticality::Critical;
} else if net_upper.contains("1.8V") || net_upper.contains("2.5V") {
return NetCriticality::High;
} else if net_upper.contains("3.3V") || net_upper.contains("5V") {
return NetCriticality::Medium;
} else if net_upper.contains("12V") || net_upper.contains("FAN") || net_upper.contains("LED") {
return NetCriticality::Low;
}
}
NetCriticality::Medium }
fn find_power_pins<'a>(&self, footprint: &'a Footprint, schematic: &Schematic) -> Vec<&'a Pad> {
let mut power_pins = Vec::new();
for pad in &footprint.pads {
if let Some(ref net_name) = pad.net_name {
let net_upper = net_name.to_uppercase();
if net_upper.contains("VDD") ||
net_upper.contains("VCC") ||
net_upper.contains("VIN") ||
net_upper.contains("POWER") ||
net_upper.contains("1.0V") ||
net_upper.contains("1.2V") ||
net_upper.contains("1.8V") ||
net_upper.contains("3.3V") ||
net_upper.contains("5V") ||
net_upper.starts_with("+") { power_pins.push(pad);
}
}
}
if power_pins.is_empty() {
for pad in &footprint.pads {
if let Some(_net_id) = pad.net {
for pcb_net in &schematic.nets {
let net_upper = pcb_net.name.to_uppercase();
if net_upper.contains("VDD") ||
net_upper.contains("VCC") ||
net_upper.contains("VIN") ||
net_upper.contains("POWER") ||
net_upper.contains("1.0V") ||
net_upper.contains("1.2V") ||
net_upper.contains("1.8V") ||
net_upper.contains("3.3V") ||
net_upper.contains("5V") ||
net_upper.starts_with("+") {
power_pins.push(pad);
break;
}
}
}
}
}
if power_pins.is_empty() && !footprint.pads.is_empty() {
if footprint.pads.len() <= 8 {
if let Some(first_pad_with_net) = footprint.pads.iter().find(|p| p.net.is_some() || p.net_name.is_some()) {
power_pins.push(first_pad_with_net);
tracing::debug!("Using fallback: assuming pad {} on {} is a power pin",
first_pad_with_net.number, footprint.reference);
}
}
}
power_pins
}
fn build_footprint_map<'a>(
&self,
_schematic: &Schematic,
pcb: &'a PcbDesign,
) -> HashMap<String, &'a Footprint> {
let mut map = HashMap::new();
for footprint in &pcb.footprints {
map.insert(footprint.reference.clone(), footprint);
}
map
}
fn find_connecting_vias<'a>(&self, footprint: &Footprint, pcb: &'a PcbDesign) -> Vec<&'a Via> {
let mut vias = Vec::new();
for pad in &footprint.pads {
if let Some(net_id) = pad.net {
for via in &pcb.vias {
if via.net == net_id {
let distance = self.distance_to_pad(&pad.position, &via.position);
if distance < 5.0 {
vias.push(via);
}
}
}
}
}
vias
}
fn check_shared_via(
&self,
footprint: &Footprint,
pcb: &PcbDesign,
footprint_map: &HashMap<String, &Footprint>,
) -> bool {
let vias = self.find_connecting_vias(footprint, pcb);
for via in vias {
let mut capacitor_count = 0;
for (ref_name, other_footprint) in footprint_map {
if ref_name.starts_with('C') || ref_name.starts_with('c') {
if other_footprint.uuid != footprint.uuid {
for pad in &other_footprint.pads {
if pad.net == Some(via.net) {
let distance = self.distance_to_pad(&pad.position, &via.position);
if distance < 2.0 {
capacitor_count += 1;
}
}
}
}
}
}
if capacitor_count > 0 {
return true; }
}
false
}
fn check_neck_down(&self, footprint: &Footprint, pcb: &PcbDesign) -> bool {
for pad in &footprint.pads {
if pad.size.width < 1.5 && pad.size.height < 1.0 {
if let Some(net_id) = pad.net {
for zone in &pcb.zones {
if zone.net == net_id {
for trace in &pcb.traces {
if trace.net == net_id {
if trace.width < 0.15 {
return true;
}
}
}
}
}
}
}
}
false
}
fn distance_to_pad(&self, pos1: &crate::parser::pcb_schema::Position3D, pos2: &crate::parser::pcb_schema::Position3D) -> f64 {
let dx = pos1.x - pos2.x;
let dy = pos1.y - pos2.y;
(dx * dx + dy * dy).sqrt()
}
fn init_ic_frequencies(&mut self) {
self.ic_switching_freqs.insert("STM32F4".to_string(), 168.0);
self.ic_switching_freqs.insert("STM32F411".to_string(), 100.0);
self.ic_switching_freqs.insert("STM32F7".to_string(), 216.0);
self.ic_switching_freqs.insert("STM32H7".to_string(), 480.0);
self.ic_switching_freqs.insert("ESP32".to_string(), 240.0);
self.ic_switching_freqs.insert("ESP32-WROOM".to_string(), 240.0);
self.ic_switching_freqs.insert("RP2040".to_string(), 133.0);
self.ic_switching_freqs.insert("ATMEGA328P".to_string(), 20.0);
self.ic_switching_freqs.insert("CPU".to_string(), 1000.0);
self.ic_switching_freqs.insert("MPU".to_string(), 1000.0);
self.ic_switching_freqs.insert("FPGA".to_string(), 500.0);
self.ic_switching_freqs.insert("DSP".to_string(), 300.0);
}
fn init_capacitor_srf(&mut self) {
self.capacitor_srf.insert("10pF".to_string(), 2000.0);
self.capacitor_srf.insert("22pF".to_string(), 1500.0);
self.capacitor_srf.insert("47pF".to_string(), 1000.0);
self.capacitor_srf.insert("100pF".to_string(), 800.0);
self.capacitor_srf.insert("220pF".to_string(), 600.0);
self.capacitor_srf.insert("470pF".to_string(), 400.0);
self.capacitor_srf.insert("1nF".to_string(), 300.0);
self.capacitor_srf.insert("2.2nF".to_string(), 200.0);
self.capacitor_srf.insert("4.7nF".to_string(), 150.0);
self.capacitor_srf.insert("10nF".to_string(), 100.0);
self.capacitor_srf.insert("22nF".to_string(), 70.0);
self.capacitor_srf.insert("47nF".to_string(), 50.0);
self.capacitor_srf.insert("100nF".to_string(), 30.0);
self.capacitor_srf.insert("220nF".to_string(), 20.0);
self.capacitor_srf.insert("470nF".to_string(), 15.0);
self.capacitor_srf.insert("1uF".to_string(), 10.0);
self.capacitor_srf.insert("2.2uF".to_string(), 7.0);
self.capacitor_srf.insert("4.7uF".to_string(), 5.0);
self.capacitor_srf.insert("10uF".to_string(), 3.0);
self.capacitor_srf.insert("22uF".to_string(), 2.0);
self.capacitor_srf.insert("47uF".to_string(), 1.5);
self.capacitor_srf.insert("100uF".to_string(), 1.0);
self.capacitor_srf.insert("0603_100nF".to_string(), 25.0);
self.capacitor_srf.insert("0603_10uF".to_string(), 2.5);
self.capacitor_srf.insert("0805_100nF".to_string(), 20.0);
self.capacitor_srf.insert("0805_10uF".to_string(), 2.0);
}
fn get_ic_switching_freq(&self, ic_value: &str) -> f64 {
let value_upper = ic_value.to_uppercase();
if let Some(&freq) = self.ic_switching_freqs.get(&value_upper) {
return freq;
}
for (pattern, freq) in &self.ic_switching_freqs {
if value_upper.contains(pattern) {
return *freq;
}
}
50.0
}
fn get_capacitor_srf(&self, cap_value: &str) -> f64 {
let value_upper = cap_value.to_uppercase();
if let Some(&srf) = self.capacitor_srf.get(&value_upper) {
return srf;
}
if let Some((num, unit)) = self.parse_capacitor_value(cap_value) {
let normalized = format!("{}{}", num, unit);
if let Some(&srf) = self.capacitor_srf.get(&normalized) {
return srf;
}
return self.interpolate_srf(num, &unit);
}
30.0
}
fn parse_capacitor_value(&self, value: &str) -> Option<(f64, String)> {
let value = value.trim();
let value_lower = value.to_lowercase();
let mut num_str = String::new();
let mut unit = String::new();
let mut found_digit = false;
for ch in value_lower.chars() {
if ch.is_ascii_digit() || ch == '.' || ch == '-' {
num_str.push(ch);
found_digit = true;
} else if found_digit {
unit.push(ch);
}
}
if num_str.is_empty() {
return None;
}
let num = num_str.parse::<f64>().ok()?;
let normalized_unit = match unit.as_str() {
"pf" | "p" => "pF",
"nf" | "n" => "nF",
"uf" | "u" | "µf" | "µ" => "uF",
"mf" | "m" => "mF",
"f" => "F",
_ => &unit,
};
Some((num, normalized_unit.to_string()))
}
fn interpolate_srf(&self, value: f64, unit: &str) -> f64 {
let capacitance_farads = match unit {
"pF" => value * 1e-12,
"nF" => value * 1e-9,
"uF" | "µF" => value * 1e-6,
"mF" => value * 1e-3,
"F" => value,
_ => value * 1e-9, };
let k = 3000.0; let srf_hz = k / capacitance_farads.sqrt();
srf_hz / 1e6 }
fn init_ic_inductance_limits(&mut self) {
self.ic_max_inductance.insert("FPGA".to_string(), 2.5);
self.ic_max_inductance.insert("STM32H7".to_string(), 3.0);
self.ic_max_inductance.insert("STM32F7".to_string(), 3.5);
self.ic_max_inductance.insert("STM32F4".to_string(), 4.0);
self.ic_max_inductance.insert("STM32F411".to_string(), 4.0);
self.ic_max_inductance.insert("ESP32".to_string(), 3.0);
self.ic_max_inductance.insert("ESP32-WROOM".to_string(), 3.0);
self.ic_max_inductance.insert("RP2040".to_string(), 4.0);
self.ic_max_inductance.insert("CPU".to_string(), 2.0);
self.ic_max_inductance.insert("MPU".to_string(), 2.0);
self.ic_max_inductance.insert("DSP".to_string(), 3.0);
self.ic_max_inductance.insert("STM32F1".to_string(), 5.0);
self.ic_max_inductance.insert("ATMEGA328P".to_string(), 10.0);
}
pub fn get_max_inductance(&self, ic_value: &str) -> Option<f64> {
let value_upper = ic_value.to_uppercase();
if let Some(&limit) = self.ic_max_inductance.get(&value_upper) {
return Some(limit);
}
for (pattern, limit) in &self.ic_max_inductance {
if value_upper.contains(pattern) {
return Some(*limit);
}
}
None
}
pub fn trace_capacitor_to_ic_path(
&self,
capacitor_ref: &str,
ic_ref: &str,
net_name: &str,
pcb: &PcbDesign,
_schematic: &Schematic,
) -> Result<PathAnalysis, PathError> {
let net_id = pcb.nets.iter()
.find(|n| n.name == net_name || n.name.replace("+", "") == net_name.replace("+", ""))
.ok_or_else(|| PathError::NetNotFound(net_name.to_string()))?
.id;
let cap_footprint = pcb.footprints.iter()
.find(|f| f.reference == capacitor_ref)
.ok_or_else(|| PathError::ComponentNotFound(format!("Capacitor {}", capacitor_ref)))?;
let ic_footprint = pcb.footprints.iter()
.find(|f| f.reference == ic_ref)
.ok_or_else(|| PathError::ComponentNotFound(format!("IC {}", ic_ref)))?;
let cap_pad = cap_footprint.pads.iter()
.find(|p| p.net == Some(net_id) || p.net_name.as_ref().map(|n| n == net_name).unwrap_or(false))
.ok_or_else(|| PathError::InvalidNetConnection)?;
let ic_pad = ic_footprint.pads.iter()
.find(|p| p.net == Some(net_id) || p.net_name.as_ref().map(|n| n == net_name).unwrap_or(false))
.ok_or_else(|| PathError::InvalidNetConnection)?;
let path = self.find_path(
&cap_pad.position,
&ic_pad.position,
net_id,
pcb,
)?;
let distance_mm = self.calculate_path_distance(&path);
let layer = self.get_primary_layer(&path);
Ok(PathAnalysis {
cap_ref: capacitor_ref.to_string(),
ic_ref: ic_ref.to_string(),
distance_mm,
layer,
path_segments: path,
net_name: net_name.to_string(),
})
}
pub fn find_all_capacitor_ic_paths(
&self,
net_name: &str,
pcb: &PcbDesign,
schematic: &Schematic,
) -> Result<Vec<PathAnalysis>, PathError> {
let net_id = pcb.nets.iter()
.find(|n| n.name == net_name || n.name.replace("+", "") == net_name.replace("+", ""))
.ok_or_else(|| PathError::NetNotFound(net_name.to_string()))?
.id;
let capacitors: Vec<&Footprint> = pcb.footprints.iter()
.filter(|f| f.reference.starts_with('C') &&
f.pads.iter().any(|p| p.net == Some(net_id)))
.collect();
let ics: Vec<&Footprint> = pcb.footprints.iter()
.filter(|f| f.reference.starts_with('U') &&
f.pads.iter().any(|p| p.net == Some(net_id)))
.collect();
let mut results = Vec::new();
for cap_fp in &capacitors {
for ic_fp in &ics {
if let Ok(path_analysis) = self.trace_capacitor_to_ic_path(
&cap_fp.reference,
&ic_fp.reference,
net_name,
pcb,
schematic,
) {
results.push(path_analysis);
}
}
}
Ok(results)
}
fn find_path(
&self,
start: &Position3D,
end: &Position3D,
net_id: u32,
pcb: &PcbDesign,
) -> Result<Vec<PathSegment>, PathError> {
let graph = self.build_connectivity_graph(net_id, pcb);
let start_node = self.find_nearest_node(start, net_id, pcb, &graph);
let end_node = self.find_nearest_node(end, net_id, pcb, &graph);
let path_nodes = self.bfs_path(&graph, &start_node, &end_node)?;
self.reconstruct_path(path_nodes, net_id, pcb)
}
fn build_connectivity_graph(
&self,
net_id: u32,
pcb: &PcbDesign,
) -> HashMap<GraphNode, Vec<GraphNode>> {
let mut graph: HashMap<GraphNode, Vec<GraphNode>> = HashMap::new();
for trace in &pcb.traces {
if trace.net == net_id {
let start_node = GraphNode::TraceEnd {
uuid: trace.uuid.clone(),
position: trace.start.clone(),
layer: trace.layer.clone(),
};
let end_node = GraphNode::TraceEnd {
uuid: trace.uuid.clone(),
position: trace.end.clone(),
layer: trace.layer.clone(),
};
graph.entry(start_node.clone()).or_insert_with(Vec::new).push(end_node.clone());
graph.entry(end_node).or_insert_with(Vec::new).push(start_node);
}
}
for via in &pcb.vias {
if via.net == net_id {
for trace in &pcb.traces {
if trace.net == net_id {
let trace_start_dist = self.distance_to_pad(&trace.start, &via.position);
let trace_end_dist = self.distance_to_pad(&trace.end, &via.position);
let via_radius = via.size / 2.0;
if trace_start_dist < via_radius + 0.1 {
let via_node = GraphNode::ViaNode {
uuid: via.uuid.clone(),
position: via.position.clone(),
layer: trace.layer.clone(),
};
let trace_node = GraphNode::TraceEnd {
uuid: trace.uuid.clone(),
position: trace.start.clone(),
layer: trace.layer.clone(),
};
graph.entry(via_node.clone()).or_insert_with(Vec::new).push(trace_node.clone());
graph.entry(trace_node).or_insert_with(Vec::new).push(via_node);
}
if trace_end_dist < via_radius + 0.1 {
let via_node = GraphNode::ViaNode {
uuid: via.uuid.clone(),
position: via.position.clone(),
layer: trace.layer.clone(),
};
let trace_node = GraphNode::TraceEnd {
uuid: trace.uuid.clone(),
position: trace.end.clone(),
layer: trace.layer.clone(),
};
graph.entry(via_node.clone()).or_insert_with(Vec::new).push(trace_node.clone());
graph.entry(trace_node).or_insert_with(Vec::new).push(via_node);
}
}
}
}
}
for zone in &pcb.zones {
if zone.net == net_id && zone.filled {
for trace in &pcb.traces {
if trace.net == net_id && trace.layer == zone.layer {
if self.point_in_zone(&trace.start, zone) {
let zone_node = GraphNode::ZoneNode {
uuid: zone.uuid.clone(),
position: trace.start.clone(),
layer: zone.layer.clone(),
};
let trace_node = GraphNode::TraceEnd {
uuid: trace.uuid.clone(),
position: trace.start.clone(),
layer: trace.layer.clone(),
};
graph.entry(zone_node.clone()).or_insert_with(Vec::new).push(trace_node.clone());
graph.entry(trace_node).or_insert_with(Vec::new).push(zone_node);
}
if self.point_in_zone(&trace.end, zone) {
let zone_node = GraphNode::ZoneNode {
uuid: zone.uuid.clone(),
position: trace.end.clone(),
layer: zone.layer.clone(),
};
let trace_node = GraphNode::TraceEnd {
uuid: trace.uuid.clone(),
position: trace.end.clone(),
layer: trace.layer.clone(),
};
graph.entry(zone_node.clone()).or_insert_with(Vec::new).push(trace_node.clone());
graph.entry(trace_node).or_insert_with(Vec::new).push(zone_node);
}
}
}
}
}
graph
}
fn find_nearest_node(
&self,
position: &Position3D,
net_id: u32,
pcb: &PcbDesign,
graph: &HashMap<GraphNode, Vec<GraphNode>>,
) -> GraphNode {
let mut nearest: Option<(&GraphNode, f64)> = None;
for node in graph.keys() {
let node_pos = match node {
GraphNode::TraceEnd { position: p, .. } => p,
GraphNode::ViaNode { position: p, .. } => p,
GraphNode::ZoneNode { position: p, .. } => p,
GraphNode::Pad { position: p, .. } => p,
};
let dist = self.distance_to_pad(position, node_pos);
if nearest.is_none() || dist < nearest.unwrap().1 {
nearest = Some((node, dist));
}
}
nearest.map(|(n, _)| n.clone())
.unwrap_or_else(|| {
for footprint in &pcb.footprints {
for pad in &footprint.pads {
if pad.net == Some(net_id) {
let pad_dist = self.distance_to_pad(&pad.position, position);
if pad_dist < 1.0 {
return GraphNode::Pad {
component_ref: footprint.reference.clone(),
pad_number: pad.number.clone(),
position: pad.position.clone(),
};
}
}
}
}
GraphNode::ZoneNode {
uuid: "start".to_string(),
position: position.clone(),
layer: "F.Cu".to_string(),
}
})
}
fn bfs_path(
&self,
graph: &HashMap<GraphNode, Vec<GraphNode>>,
start: &GraphNode,
end: &GraphNode,
) -> Result<Vec<GraphNode>, PathError> {
let mut queue = VecDeque::new();
let mut visited = HashSet::new();
let mut parent: HashMap<GraphNode, GraphNode> = HashMap::new();
queue.push_back(start.clone());
visited.insert(start.clone());
while let Some(current) = queue.pop_front() {
if current == *end {
let mut path = Vec::new();
let mut node = end.clone();
path.push(node.clone());
while let Some(p) = parent.get(&node) {
path.push(p.clone());
node = p.clone();
}
path.reverse();
return Ok(path);
}
if let Some(neighbors) = graph.get(¤t) {
for neighbor in neighbors {
if !visited.contains(neighbor) {
visited.insert(neighbor.clone());
parent.insert(neighbor.clone(), current.clone());
queue.push_back(neighbor.clone());
}
}
}
}
Err(PathError::NoPathFound)
}
fn reconstruct_path(
&self,
nodes: Vec<GraphNode>,
_net_id: u32,
pcb: &PcbDesign,
) -> Result<Vec<PathSegment>, PathError> {
let mut segments = Vec::new();
for i in 0..nodes.len() {
match &nodes[i] {
GraphNode::Pad { component_ref, pad_number, position } => {
segments.push(PathSegment::Pad {
component_ref: component_ref.clone(),
pad_number: pad_number.clone(),
position: position.clone(),
});
}
GraphNode::TraceEnd { uuid, position: _, layer } => {
if let Some(trace) = pcb.traces.iter().find(|t| t.uuid == *uuid) {
if i == 0 || !matches!(nodes[i-1], GraphNode::TraceEnd { uuid: ref u, .. } if u == uuid) {
segments.push(PathSegment::Trace {
uuid: uuid.clone(),
start: trace.start.clone(),
end: trace.end.clone(),
length_mm: trace.length(),
layer: layer.clone(),
});
}
}
}
GraphNode::ViaNode { uuid, position, layer: _ } => {
if let Some(via) = pcb.vias.iter().find(|v| v.uuid == *uuid) {
segments.push(PathSegment::Via {
uuid: uuid.clone(),
position: position.clone(),
layers: via.layers.clone(),
});
}
}
GraphNode::ZoneNode { uuid, position, layer } => {
let entry = position.clone();
let exit = if i + 1 < nodes.len() {
match &nodes[i + 1] {
GraphNode::TraceEnd { position: p, .. } |
GraphNode::ViaNode { position: p, .. } |
GraphNode::Pad { position: p, .. } |
GraphNode::ZoneNode { position: p, .. } => p.clone(),
}
} else {
entry.clone()
};
let zone_dist = self.distance_to_pad(&entry, &exit);
segments.push(PathSegment::Zone {
uuid: uuid.clone(),
layer: layer.clone(),
entry_point: entry,
exit_point: exit,
distance_mm: zone_dist,
});
}
}
}
Ok(segments)
}
fn calculate_path_distance(&self, segments: &[PathSegment]) -> f64 {
let mut total = 0.0;
for segment in segments {
match segment {
PathSegment::Trace { length_mm, .. } => {
total += length_mm;
}
PathSegment::Via { .. } => {
total += 0.1;
}
PathSegment::Zone { distance_mm, .. } => {
total += distance_mm;
}
PathSegment::Pad { .. } => {
}
}
}
total
}
fn get_primary_layer(&self, segments: &[PathSegment]) -> String {
let mut layer_counts: HashMap<String, usize> = HashMap::new();
for segment in segments {
match segment {
PathSegment::Trace { layer, .. } => {
*layer_counts.entry(layer.clone()).or_insert(0) += 1;
}
PathSegment::Zone { layer, .. } => {
*layer_counts.entry(layer.clone()).or_insert(0) += 1;
}
PathSegment::Via { layers, .. } => {
*layer_counts.entry(layers.0.clone()).or_insert(0) += 1;
*layer_counts.entry(layers.1.clone()).or_insert(0) += 1;
}
_ => {}
}
}
layer_counts.into_iter()
.max_by_key(|(_, count)| *count)
.map(|(layer, _)| layer)
.unwrap_or_else(|| "F.Cu".to_string())
}
fn point_in_zone(&self, point: &Position3D, zone: &Zone) -> bool {
if zone.outline.is_empty() {
return false;
}
let min_x = zone.outline.iter().map(|p| p.x).fold(f64::INFINITY, f64::min);
let max_x = zone.outline.iter().map(|p| p.x).fold(f64::NEG_INFINITY, f64::max);
let min_y = zone.outline.iter().map(|p| p.y).fold(f64::INFINITY, f64::min);
let max_y = zone.outline.iter().map(|p| p.y).fold(f64::NEG_INFINITY, f64::max);
point.x >= min_x && point.x <= max_x && point.y >= min_y && point.y <= max_y
}
}
impl Default for DRSAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::schema::{Schematic, Component, Position};
use crate::parser::pcb_schema::{PcbDesign, Footprint, Pad, Position3D, Size2D, Trace};
use std::collections::HashMap;
#[test]
fn test_net_criticality_weights() {
assert_eq!(NetCriticality::Critical.weight(), 1.0);
assert_eq!(NetCriticality::High.weight(), 0.7);
assert_eq!(NetCriticality::Medium.weight(), 0.5);
assert_eq!(NetCriticality::Low.weight(), 0.2);
}
#[test]
fn test_ic_switching_freq() {
let analyzer = DRSAnalyzer::new();
assert_eq!(analyzer.get_ic_switching_freq("STM32F411"), 100.0);
assert_eq!(analyzer.get_ic_switching_freq("ESP32"), 240.0);
assert_eq!(analyzer.get_ic_switching_freq("UnknownIC"), 50.0);
}
#[test]
fn test_capacitor_srf() {
let analyzer = DRSAnalyzer::new();
assert_eq!(analyzer.get_capacitor_srf("100nF"), 30.0);
assert_eq!(analyzer.get_capacitor_srf("10nF"), 100.0);
assert_eq!(analyzer.get_capacitor_srf("1uF"), 10.0);
}
#[test]
fn test_parse_capacitor_value() {
let analyzer = DRSAnalyzer::new();
assert_eq!(analyzer.parse_capacitor_value("100nF"), Some((100.0, "nF".to_string())));
assert_eq!(analyzer.parse_capacitor_value("2.2uF"), Some((2.2, "uF".to_string())));
assert_eq!(analyzer.parse_capacitor_value("47pF"), Some((47.0, "pF".to_string())));
}
fn create_test_pcb() -> PcbDesign {
let mut pcb = PcbDesign::default();
pcb.uuid = "test-pcb".to_string();
pcb.filename = "test.kicad_pcb".to_string();
pcb.nets.push(crate::parser::pcb_schema::PcbNet {
id: 1,
name: "+3V3".to_string(),
});
pcb
}
fn create_test_schematic() -> Schematic {
Schematic {
uuid: "test-schematic".to_string(),
filename: "test.kicad_sch".to_string(),
version: Some("20231120".to_string()),
components: vec![
Component {
uuid: "c1-uuid".to_string(),
reference: "C1".to_string(),
value: "100nF".to_string(),
lib_id: "Device:C".to_string(),
footprint: None,
position: Position { x: 100.0, y: 100.0 },
rotation: 0.0,
properties: HashMap::new(),
pins: vec![],
},
Component {
uuid: "u1-uuid".to_string(),
reference: "U1".to_string(),
value: "STM32F411".to_string(),
lib_id: "MCU:STM32F4".to_string(),
footprint: None,
position: Position { x: 150.0, y: 100.0 },
rotation: 0.0,
properties: HashMap::new(),
pins: vec![],
},
],
wires: vec![],
labels: vec![],
nets: vec![],
power_symbols: vec![],
}
}
fn create_capacitor_footprint(reference: &str, x: f64, y: f64, net_id: u32) -> Footprint {
Footprint {
uuid: format!("{}-fp-uuid", reference.to_lowercase()),
reference: reference.to_string(),
value: "100nF".to_string(),
footprint_lib: "Capacitor_SMD:C_0402".to_string(),
layer: "F.Cu".to_string(),
position: Position3D::new(x, y),
rotation: 0.0,
pads: vec![
Pad {
number: "1".to_string(),
pad_type: crate::parser::pcb_schema::PadType::SMD,
shape: crate::parser::pcb_schema::PadShape::Rect,
position: Position3D::new(x - 0.5, y),
size: Size2D { width: 0.6, height: 0.6 },
drill: None,
layers: vec!["F.Cu".to_string()],
net: Some(net_id),
net_name: Some("+3V3".to_string()),
},
Pad {
number: "2".to_string(),
pad_type: crate::parser::pcb_schema::PadType::SMD,
shape: crate::parser::pcb_schema::PadShape::Rect,
position: Position3D::new(x + 0.5, y),
size: Size2D { width: 0.6, height: 0.6 },
drill: None,
layers: vec!["F.Cu".to_string()],
net: Some(2), net_name: Some("GND".to_string()),
},
],
properties: HashMap::new(),
}
}
fn create_ic_footprint(reference: &str, x: f64, y: f64, net_id: u32) -> Footprint {
Footprint {
uuid: format!("{}-fp-uuid", reference.to_lowercase()),
reference: reference.to_string(),
value: "STM32F411".to_string(),
footprint_lib: "Package_QFP:LQFP-48".to_string(),
layer: "F.Cu".to_string(),
position: Position3D::new(x, y),
rotation: 0.0,
pads: vec![
Pad {
number: "1".to_string(),
pad_type: crate::parser::pcb_schema::PadType::SMD,
shape: crate::parser::pcb_schema::PadShape::Rect,
position: Position3D::new(x - 2.0, y - 2.0),
size: Size2D { width: 0.5, height: 0.5 },
drill: None,
layers: vec!["F.Cu".to_string()],
net: Some(net_id),
net_name: Some("+3V3".to_string()),
},
Pad {
number: "2".to_string(),
pad_type: crate::parser::pcb_schema::PadType::SMD,
shape: crate::parser::pcb_schema::PadShape::Rect,
position: Position3D::new(x - 1.0, y - 2.0),
size: Size2D { width: 0.5, height: 0.5 },
drill: None,
layers: vec!["F.Cu".to_string()],
net: Some(2), net_name: Some("GND".to_string()),
},
],
properties: HashMap::new(),
}
}
#[test]
fn test_path_tracing_simple_trace() {
let analyzer = DRSAnalyzer::new();
let mut pcb = create_test_pcb();
let schematic = create_test_schematic();
pcb.footprints.push(create_capacitor_footprint("C1", 100.0, 100.0, 1));
pcb.footprints.push(create_ic_footprint("U1", 150.0, 100.0, 1));
pcb.traces.push(Trace {
uuid: "trace1-uuid".to_string(),
start: Position3D::new(100.5, 100.0),
end: Position3D::new(148.0, 100.0),
width: 0.3,
layer: "F.Cu".to_string(),
net: 1,
net_name: Some("+3V3".to_string()),
locked: false,
});
let result = analyzer.trace_capacitor_to_ic_path("C1", "U1", "+3V3", &pcb, &schematic);
assert!(result.is_ok(), "Should find path through trace");
let path = result.unwrap();
assert_eq!(path.cap_ref, "C1");
assert_eq!(path.ic_ref, "U1");
assert_eq!(path.net_name, "+3V3");
assert!(path.distance_mm > 0.0, "Distance should be positive");
assert!(!path.path_segments.is_empty(), "Should have path segments");
let has_trace = path.path_segments.iter().any(|s| matches!(s, PathSegment::Trace { .. }));
assert!(has_trace, "Path should contain at least one trace segment");
}
#[test]
fn test_path_tracing_with_via() {
let analyzer = DRSAnalyzer::new();
let mut pcb = create_test_pcb();
let schematic = create_test_schematic();
pcb.footprints.push(create_capacitor_footprint("C1", 100.0, 100.0, 1));
pcb.footprints.push(create_ic_footprint("U1", 150.0, 100.0, 1));
pcb.traces.push(Trace {
uuid: "trace1-uuid".to_string(),
start: Position3D::new(100.5, 100.0),
end: Position3D::new(120.0, 100.0),
width: 0.3,
layer: "F.Cu".to_string(),
net: 1,
net_name: Some("+3V3".to_string()),
locked: false,
});
pcb.vias.push(Via {
uuid: "via1-uuid".to_string(),
position: Position3D::new(120.0, 100.0),
size: 0.5,
drill: 0.2,
layers: ("F.Cu".to_string(), "In1.Cu".to_string()),
net: 1,
net_name: Some("+3V3".to_string()),
via_type: crate::parser::pcb_schema::ViaType::Through,
locked: false,
});
pcb.traces.push(Trace {
uuid: "trace2-uuid".to_string(),
start: Position3D::new(120.0, 100.0),
end: Position3D::new(148.0, 100.0),
width: 0.3,
layer: "In1.Cu".to_string(),
net: 1,
net_name: Some("+3V3".to_string()),
locked: false,
});
let result = analyzer.trace_capacitor_to_ic_path("C1", "U1", "+3V3", &pcb, &schematic);
assert!(result.is_ok(), "Should find path through trace and via");
let path = result.unwrap();
let has_via = path.path_segments.iter().any(|s| matches!(s, PathSegment::Via { .. }));
assert!(has_via, "Path should contain at least one via segment");
let trace_count = path.path_segments.iter()
.filter(|s| matches!(s, PathSegment::Trace { .. }))
.count();
assert!(trace_count >= 1, "Path should contain trace segments");
}
#[test]
fn test_path_tracing_with_zone() {
let analyzer = DRSAnalyzer::new();
let mut pcb = create_test_pcb();
let schematic = create_test_schematic();
pcb.footprints.push(create_capacitor_footprint("C1", 100.0, 100.0, 1));
pcb.footprints.push(create_ic_footprint("U1", 200.0, 100.0, 1));
pcb.traces.push(Trace {
uuid: "trace1-uuid".to_string(),
start: Position3D::new(100.5, 100.0),
end: Position3D::new(110.0, 100.0),
width: 0.3,
layer: "F.Cu".to_string(),
net: 1,
net_name: Some("+3V3".to_string()),
locked: false,
});
pcb.vias.push(Via {
uuid: "via1-uuid".to_string(),
position: Position3D::new(110.0, 100.0),
size: 0.5,
drill: 0.2,
layers: ("F.Cu".to_string(), "In1.Cu".to_string()),
net: 1,
net_name: Some("+3V3".to_string()),
via_type: crate::parser::pcb_schema::ViaType::Through,
locked: false,
});
pcb.traces.push(Trace {
uuid: "trace-zone-uuid".to_string(),
start: Position3D::new(110.0, 100.0),
end: Position3D::new(190.0, 100.0),
width: 0.3,
layer: "In1.Cu".to_string(),
net: 1,
net_name: Some("+3V3".to_string()),
locked: false,
});
pcb.zones.push(Zone {
uuid: "zone1-uuid".to_string(),
net: 1,
net_name: "+3V3".to_string(),
layer: "In1.Cu".to_string(),
priority: 0,
connect_pads: crate::parser::pcb_schema::ZoneConnectType::Solid,
min_thickness: 0.0,
filled: true,
outline: vec![
Position3D::new(105.0, 95.0),
Position3D::new(195.0, 95.0),
Position3D::new(195.0, 105.0),
Position3D::new(105.0, 105.0),
],
filled_polygons: vec![],
keepout: None,
});
pcb.vias.push(Via {
uuid: "via2-uuid".to_string(),
position: Position3D::new(190.0, 100.0),
size: 0.5,
drill: 0.2,
layers: ("F.Cu".to_string(), "In1.Cu".to_string()),
net: 1,
net_name: Some("+3V3".to_string()),
via_type: crate::parser::pcb_schema::ViaType::Through,
locked: false,
});
pcb.traces.push(Trace {
uuid: "trace2-uuid".to_string(),
start: Position3D::new(190.0, 100.0),
end: Position3D::new(198.0, 100.0),
width: 0.3,
layer: "F.Cu".to_string(),
net: 1,
net_name: Some("+3V3".to_string()),
locked: false,
});
let result = analyzer.trace_capacitor_to_ic_path("C1", "U1", "+3V3", &pcb, &schematic);
if result.is_ok() {
let path = result.unwrap();
let via_count = path.path_segments.iter()
.filter(|s| matches!(s, PathSegment::Via { .. }))
.count();
assert!(via_count >= 1, "Path should contain via segments");
let has_zone = path.path_segments.iter().any(|s| matches!(s, PathSegment::Zone { .. }));
if has_zone {
println!("Zone segment found in path");
}
} else {
println!("Path finding failed (zone connectivity may need refinement): {:?}", result.unwrap_err());
}
}
#[test]
fn test_path_tracing_missing_component() {
let analyzer = DRSAnalyzer::new();
let mut pcb = create_test_pcb();
let schematic = create_test_schematic();
pcb.footprints.push(create_ic_footprint("U1", 150.0, 100.0, 1));
let result = analyzer.trace_capacitor_to_ic_path("C1", "U1", "+3V3", &pcb, &schematic);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), PathError::ComponentNotFound(_)));
}
#[test]
fn test_path_tracing_missing_net() {
let analyzer = DRSAnalyzer::new();
let mut pcb = create_test_pcb();
let schematic = create_test_schematic();
pcb.nets.clear();
pcb.footprints.push(create_capacitor_footprint("C1", 100.0, 100.0, 1));
pcb.footprints.push(create_ic_footprint("U1", 150.0, 100.0, 1));
let result = analyzer.trace_capacitor_to_ic_path("C1", "U1", "+3V3", &pcb, &schematic);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), PathError::NetNotFound(_)));
}
#[test]
fn test_path_tracing_no_path() {
let analyzer = DRSAnalyzer::new();
let mut pcb = create_test_pcb();
let schematic = create_test_schematic();
pcb.footprints.push(create_capacitor_footprint("C1", 100.0, 100.0, 1));
pcb.footprints.push(create_ic_footprint("U1", 150.0, 100.0, 1));
let result = analyzer.trace_capacitor_to_ic_path("C1", "U1", "+3V3", &pcb, &schematic);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), PathError::NoPathFound));
}
#[test]
fn test_find_all_capacitor_ic_paths() {
let analyzer = DRSAnalyzer::new();
let mut pcb = create_test_pcb();
let schematic = create_test_schematic();
pcb.footprints.push(create_capacitor_footprint("C1", 100.0, 100.0, 1));
pcb.footprints.push(create_capacitor_footprint("C2", 110.0, 100.0, 1));
pcb.footprints.push(create_ic_footprint("U1", 150.0, 100.0, 1));
pcb.footprints.push(create_ic_footprint("U2", 160.0, 100.0, 1));
pcb.traces.push(Trace {
uuid: "trace1-uuid".to_string(),
start: Position3D::new(100.5, 100.0),
end: Position3D::new(148.0, 100.0),
width: 0.3,
layer: "F.Cu".to_string(),
net: 1,
net_name: Some("+3V3".to_string()),
locked: false,
});
pcb.traces.push(Trace {
uuid: "trace2-uuid".to_string(),
start: Position3D::new(110.5, 100.0),
end: Position3D::new(158.0, 100.0),
width: 0.3,
layer: "F.Cu".to_string(),
net: 1,
net_name: Some("+3V3".to_string()),
locked: false,
});
let result = analyzer.find_all_capacitor_ic_paths("+3V3", &pcb, &schematic);
assert!(result.is_ok());
let paths = result.unwrap();
assert!(paths.len() >= 2, "Should find multiple capacitor-IC paths");
for path in &paths {
assert!(path.cap_ref.starts_with('C'), "Capacitor ref should start with C");
assert!(path.ic_ref.starts_with('U'), "IC ref should start with U");
assert_eq!(path.net_name, "+3V3");
assert!(path.distance_mm > 0.0);
assert!(!path.layer.is_empty());
}
}
#[test]
fn test_path_distance_calculation() {
let analyzer = DRSAnalyzer::new();
let segments = vec![
PathSegment::Trace {
uuid: "trace1".to_string(),
start: Position3D::new(0.0, 0.0),
end: Position3D::new(10.0, 0.0),
length_mm: 10.0,
layer: "F.Cu".to_string(),
},
PathSegment::Via {
uuid: "via1".to_string(),
position: Position3D::new(10.0, 0.0),
layers: ("F.Cu".to_string(), "In1.Cu".to_string()),
},
PathSegment::Trace {
uuid: "trace2".to_string(),
start: Position3D::new(10.0, 0.0),
end: Position3D::new(20.0, 0.0),
length_mm: 10.0,
layer: "In1.Cu".to_string(),
},
];
let distance = analyzer.calculate_path_distance(&segments);
assert!((distance - 20.1).abs() < 0.01, "Distance should be approximately 20.1mm");
}
#[test]
fn test_primary_layer_detection() {
let analyzer = DRSAnalyzer::new();
let segments = vec![
PathSegment::Trace {
uuid: "trace1".to_string(),
start: Position3D::new(0.0, 0.0),
end: Position3D::new(10.0, 0.0),
length_mm: 10.0,
layer: "F.Cu".to_string(),
},
PathSegment::Trace {
uuid: "trace2".to_string(),
start: Position3D::new(10.0, 0.0),
end: Position3D::new(20.0, 0.0),
length_mm: 10.0,
layer: "F.Cu".to_string(),
},
PathSegment::Via {
uuid: "via1".to_string(),
position: Position3D::new(20.0, 0.0),
layers: ("F.Cu".to_string(), "In1.Cu".to_string()),
},
PathSegment::Trace {
uuid: "trace3".to_string(),
start: Position3D::new(20.0, 0.0),
end: Position3D::new(25.0, 0.0),
length_mm: 5.0,
layer: "In1.Cu".to_string(),
},
];
let primary_layer = analyzer.get_primary_layer(&segments);
assert_eq!(primary_layer, "F.Cu");
}
#[test]
fn test_point_in_zone() {
let analyzer = DRSAnalyzer::new();
let zone = Zone {
uuid: "zone1".to_string(),
net: 1,
net_name: "+3V3".to_string(),
layer: "In1.Cu".to_string(),
priority: 0,
connect_pads: crate::parser::pcb_schema::ZoneConnectType::Solid,
min_thickness: 0.0,
filled: true,
outline: vec![
Position3D::new(0.0, 0.0),
Position3D::new(100.0, 0.0),
Position3D::new(100.0, 100.0),
Position3D::new(0.0, 100.0),
],
filled_polygons: vec![],
keepout: None,
};
let point_inside = Position3D::new(50.0, 50.0);
assert!(analyzer.point_in_zone(&point_inside, &zone), "Point should be inside zone");
let point_outside = Position3D::new(150.0, 150.0);
assert!(!analyzer.point_in_zone(&point_outside, &zone), "Point should be outside zone");
let point_boundary = Position3D::new(0.0, 50.0);
assert!(analyzer.point_in_zone(&point_boundary, &zone), "Point on boundary should be considered inside");
}
#[test]
fn test_path_tracing_complex_routing() {
let analyzer = DRSAnalyzer::new();
let mut pcb = create_test_pcb();
let schematic = create_test_schematic();
pcb.footprints.push(create_capacitor_footprint("C1", 100.0, 100.0, 1));
pcb.footprints.push(create_ic_footprint("U1", 200.0, 150.0, 1));
pcb.traces.push(Trace {
uuid: "trace1-uuid".to_string(),
start: Position3D::new(100.5, 100.0),
end: Position3D::new(110.0, 100.0),
width: 0.3,
layer: "F.Cu".to_string(),
net: 1,
net_name: Some("+3V3".to_string()),
locked: false,
});
pcb.vias.push(Via {
uuid: "via1-uuid".to_string(),
position: Position3D::new(110.0, 100.0),
size: 0.5,
drill: 0.2,
layers: ("F.Cu".to_string(), "In1.Cu".to_string()),
net: 1,
net_name: Some("+3V3".to_string()),
via_type: crate::parser::pcb_schema::ViaType::Through,
locked: false,
});
pcb.zones.push(Zone {
uuid: "zone1-uuid".to_string(),
net: 1,
net_name: "+3V3".to_string(),
layer: "In1.Cu".to_string(),
priority: 0,
connect_pads: crate::parser::pcb_schema::ZoneConnectType::Solid,
min_thickness: 0.0,
filled: true,
outline: vec![
Position3D::new(105.0, 95.0),
Position3D::new(195.0, 95.0),
Position3D::new(195.0, 155.0),
Position3D::new(105.0, 155.0),
],
filled_polygons: vec![],
keepout: None,
});
pcb.vias.push(Via {
uuid: "via2-uuid".to_string(),
position: Position3D::new(190.0, 150.0),
size: 0.5,
drill: 0.2,
layers: ("F.Cu".to_string(), "In1.Cu".to_string()),
net: 1,
net_name: Some("+3V3".to_string()),
via_type: crate::parser::pcb_schema::ViaType::Through,
locked: false,
});
pcb.traces.push(Trace {
uuid: "trace2-uuid".to_string(),
start: Position3D::new(190.0, 150.0),
end: Position3D::new(198.0, 150.0),
width: 0.3,
layer: "F.Cu".to_string(),
net: 1,
net_name: Some("+3V3".to_string()),
locked: false,
});
pcb.traces.push(Trace {
uuid: "trace-zone-uuid".to_string(),
start: Position3D::new(110.0, 100.0),
end: Position3D::new(190.0, 150.0),
width: 0.3,
layer: "In1.Cu".to_string(),
net: 1,
net_name: Some("+3V3".to_string()),
locked: false,
});
let result = analyzer.trace_capacitor_to_ic_path("C1", "U1", "+3V3", &pcb, &schematic);
assert!(result.is_ok(), "Should find complex path");
let path = result.unwrap();
let has_trace = path.path_segments.iter().any(|s| matches!(s, PathSegment::Trace { .. }));
let has_via = path.path_segments.iter().any(|s| matches!(s, PathSegment::Via { .. }));
assert!(has_trace, "Path should contain trace segments");
assert!(has_via, "Path should contain via segments");
let has_zone = path.path_segments.iter().any(|s| matches!(s, PathSegment::Zone { .. }));
if has_zone {
println!("Zone segment found in complex routing path");
}
let dx: f64 = 200.0 - 100.0;
let dy: f64 = 150.0 - 100.0;
let euclidean_dist = (dx * dx + dy * dy).sqrt();
assert!(path.distance_mm >= euclidean_dist * 0.8, "Path distance should be close to or greater than Euclidean");
}
#[test]
fn test_path_tracing_invalid_net_connection() {
let analyzer = DRSAnalyzer::new();
let mut pcb = create_test_pcb();
let schematic = create_test_schematic();
let mut cap_fp = create_capacitor_footprint("C1", 100.0, 100.0, 1);
cap_fp.pads[0].net = Some(99); cap_fp.pads[0].net_name = Some("WRONG_NET".to_string());
pcb.footprints.push(cap_fp);
pcb.footprints.push(create_ic_footprint("U1", 150.0, 100.0, 1));
let result = analyzer.trace_capacitor_to_ic_path("C1", "U1", "+3V3", &pcb, &schematic);
assert!(result.is_err(), "Should fail when capacitor pad is not on the specified net");
let err = result.unwrap_err();
assert!(
matches!(err, PathError::InvalidNetConnection) || matches!(err, PathError::NoPathFound),
"Should fail with InvalidNetConnection or NoPathFound, got: {:?}", err
);
}
#[test]
fn test_path_tracing_empty_graph() {
let analyzer = DRSAnalyzer::new();
let mut pcb = create_test_pcb();
let schematic = create_test_schematic();
pcb.footprints.push(create_capacitor_footprint("C1", 100.0, 100.0, 1));
pcb.footprints.push(create_ic_footprint("U1", 150.0, 100.0, 1));
let result = analyzer.trace_capacitor_to_ic_path("C1", "U1", "+3V3", &pcb, &schematic);
assert!(result.is_err(), "Should fail when no traces/vias/zones exist");
assert!(matches!(result.unwrap_err(), PathError::NoPathFound));
}
#[test]
fn test_path_tracing_net_name_variations() {
let analyzer = DRSAnalyzer::new();
let mut pcb = create_test_pcb();
let schematic = create_test_schematic();
pcb.nets[0].name = "3V3".to_string();
pcb.footprints.push(create_capacitor_footprint("C1", 100.0, 100.0, 1));
pcb.footprints.push(create_ic_footprint("U1", 150.0, 100.0, 1));
pcb.traces.push(Trace {
uuid: "trace1-uuid".to_string(),
start: Position3D::new(100.5, 100.0),
end: Position3D::new(148.0, 100.0),
width: 0.3,
layer: "F.Cu".to_string(),
net: 1,
net_name: Some("3V3".to_string()),
locked: false,
});
let result = analyzer.trace_capacitor_to_ic_path("C1", "U1", "+3V3", &pcb, &schematic);
assert!(result.is_ok(), "Should handle net name variations");
}
}