Some checks are pending
CI / Lint and Format (push) Waiting to run
CI / Test Python 3.11 on macos-latest (push) Waiting to run
CI / Test Python 3.12 on macos-latest (push) Waiting to run
CI / Test Python 3.13 on macos-latest (push) Waiting to run
CI / Test Python 3.10 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.11 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.12 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.13 on ubuntu-latest (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / Build Package (push) Blocked by required conditions
Add intelligent analysis and recommendation tools for KiCad designs: ## New AI Tools (kicad_mcp/tools/ai_tools.py) - suggest_components_for_circuit: Smart component suggestions based on circuit analysis - recommend_design_rules: Automated design rule recommendations for different technologies - optimize_pcb_layout: PCB layout optimization for signal integrity, thermal, and cost - analyze_design_completeness: Comprehensive design completeness analysis ## Enhanced Utilities - component_utils.py: Add ComponentType enum and component classification functions - pattern_recognition.py: Enhanced circuit pattern analysis and recommendations - netlist_parser.py: Implement missing parse_netlist_file function for AI tools ## Key Features - Circuit pattern recognition for power supplies, amplifiers, microcontrollers - Technology-specific design rules (standard, HDI, RF, automotive) - Layout optimization suggestions with implementation steps - Component suggestion system with standard values and examples - Design completeness scoring with actionable recommendations ## Server Integration - Register AI tools in FastMCP server - Integrate with existing KiCad utilities and file parsers - Error handling and graceful fallbacks for missing data Fixes ImportError that prevented server startup and enables advanced AI-powered design assistance for KiCad projects. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
445 lines
16 KiB
Python
445 lines
16 KiB
Python
"""
|
|
Advanced DRC (Design Rule Check) utilities for KiCad.
|
|
|
|
Provides sophisticated DRC rule creation, customization, and validation
|
|
beyond the basic KiCad DRC capabilities.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
import logging
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RuleSeverity(Enum):
|
|
"""DRC rule severity levels."""
|
|
ERROR = "error"
|
|
WARNING = "warning"
|
|
INFO = "info"
|
|
IGNORE = "ignore"
|
|
|
|
|
|
class RuleType(Enum):
|
|
"""Types of DRC rules."""
|
|
CLEARANCE = "clearance"
|
|
TRACK_WIDTH = "track_width"
|
|
VIA_SIZE = "via_size"
|
|
ANNULAR_RING = "annular_ring"
|
|
DRILL_SIZE = "drill_size"
|
|
COURTYARD_CLEARANCE = "courtyard_clearance"
|
|
SILK_CLEARANCE = "silk_clearance"
|
|
FABRICATION = "fabrication"
|
|
ASSEMBLY = "assembly"
|
|
ELECTRICAL = "electrical"
|
|
MECHANICAL = "mechanical"
|
|
|
|
|
|
@dataclass
|
|
class DRCRule:
|
|
"""Represents a single DRC rule."""
|
|
name: str
|
|
rule_type: RuleType
|
|
severity: RuleSeverity
|
|
constraint: dict[str, Any]
|
|
condition: str | None = None # Expression for when rule applies
|
|
description: str | None = None
|
|
enabled: bool = True
|
|
custom_message: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class DRCRuleSet:
|
|
"""Collection of DRC rules with metadata."""
|
|
name: str
|
|
version: str
|
|
description: str
|
|
rules: list[DRCRule] = field(default_factory=list)
|
|
technology: str | None = None # e.g., "PCB", "Flex", "HDI"
|
|
layer_count: int | None = None
|
|
board_thickness: float | None = None
|
|
created_by: str | None = None
|
|
|
|
|
|
class AdvancedDRCManager:
|
|
"""Manager for advanced DRC rules and validation."""
|
|
|
|
def __init__(self):
|
|
"""Initialize the DRC manager."""
|
|
self.rule_sets = {}
|
|
self.active_rule_set = None
|
|
self._load_default_rules()
|
|
|
|
def _load_default_rules(self) -> None:
|
|
"""Load default DRC rule sets."""
|
|
# Standard PCB rules
|
|
standard_rules = DRCRuleSet(
|
|
name="Standard PCB",
|
|
version="1.0",
|
|
description="Standard PCB manufacturing rules",
|
|
technology="PCB"
|
|
)
|
|
|
|
# Basic clearance rules
|
|
standard_rules.rules.extend([
|
|
DRCRule(
|
|
name="Min Track Width",
|
|
rule_type=RuleType.TRACK_WIDTH,
|
|
severity=RuleSeverity.ERROR,
|
|
constraint={"min_width": 0.1}, # 0.1mm minimum
|
|
description="Minimum track width for manufacturability"
|
|
),
|
|
DRCRule(
|
|
name="Standard Clearance",
|
|
rule_type=RuleType.CLEARANCE,
|
|
severity=RuleSeverity.ERROR,
|
|
constraint={"min_clearance": 0.2}, # 0.2mm minimum
|
|
description="Standard clearance between conductors"
|
|
),
|
|
DRCRule(
|
|
name="Via Drill Size",
|
|
rule_type=RuleType.VIA_SIZE,
|
|
severity=RuleSeverity.ERROR,
|
|
constraint={"min_drill": 0.2, "max_drill": 6.0},
|
|
description="Via drill size constraints"
|
|
),
|
|
DRCRule(
|
|
name="Via Annular Ring",
|
|
rule_type=RuleType.ANNULAR_RING,
|
|
severity=RuleSeverity.WARNING,
|
|
constraint={"min_annular_ring": 0.05}, # 0.05mm minimum
|
|
description="Minimum annular ring for vias"
|
|
)
|
|
])
|
|
|
|
self.rule_sets["standard"] = standard_rules
|
|
self.active_rule_set = "standard"
|
|
|
|
def create_high_density_rules(self) -> DRCRuleSet:
|
|
"""Create rules for high-density interconnect (HDI) boards."""
|
|
hdi_rules = DRCRuleSet(
|
|
name="HDI PCB",
|
|
version="1.0",
|
|
description="High-density interconnect PCB rules",
|
|
technology="HDI"
|
|
)
|
|
|
|
hdi_rules.rules.extend([
|
|
DRCRule(
|
|
name="HDI Track Width",
|
|
rule_type=RuleType.TRACK_WIDTH,
|
|
severity=RuleSeverity.ERROR,
|
|
constraint={"min_width": 0.075}, # 75μm minimum
|
|
description="Minimum track width for HDI manufacturing"
|
|
),
|
|
DRCRule(
|
|
name="HDI Clearance",
|
|
rule_type=RuleType.CLEARANCE,
|
|
severity=RuleSeverity.ERROR,
|
|
constraint={"min_clearance": 0.075}, # 75μm minimum
|
|
description="Minimum clearance for HDI boards"
|
|
),
|
|
DRCRule(
|
|
name="Microvia Size",
|
|
rule_type=RuleType.VIA_SIZE,
|
|
severity=RuleSeverity.ERROR,
|
|
constraint={"min_drill": 0.1, "max_drill": 0.15},
|
|
description="Microvia drill size constraints"
|
|
),
|
|
DRCRule(
|
|
name="BGA Escape Routing",
|
|
rule_type=RuleType.CLEARANCE,
|
|
severity=RuleSeverity.WARNING,
|
|
constraint={"min_clearance": 0.1},
|
|
condition="A.intersects(B.Type == 'BGA')",
|
|
description="Clearance around BGA escape routes"
|
|
)
|
|
])
|
|
|
|
return hdi_rules
|
|
|
|
def create_rf_rules(self) -> DRCRuleSet:
|
|
"""Create rules specifically for RF/microwave designs."""
|
|
rf_rules = DRCRuleSet(
|
|
name="RF/Microwave",
|
|
version="1.0",
|
|
description="Rules for RF and microwave PCB designs",
|
|
technology="RF"
|
|
)
|
|
|
|
rf_rules.rules.extend([
|
|
DRCRule(
|
|
name="Controlled Impedance Spacing",
|
|
rule_type=RuleType.CLEARANCE,
|
|
severity=RuleSeverity.ERROR,
|
|
constraint={"min_clearance": 0.2},
|
|
condition="A.NetClass == 'RF' or B.NetClass == 'RF'",
|
|
description="Spacing for controlled impedance traces"
|
|
),
|
|
DRCRule(
|
|
name="RF Via Stitching",
|
|
rule_type=RuleType.VIA_SIZE,
|
|
severity=RuleSeverity.WARNING,
|
|
constraint={"max_spacing": 2.0}, # Via stitching spacing
|
|
condition="Layer == 'Ground'",
|
|
description="Ground via stitching for RF designs"
|
|
),
|
|
DRCRule(
|
|
name="Microstrip Width Control",
|
|
rule_type=RuleType.TRACK_WIDTH,
|
|
severity=RuleSeverity.ERROR,
|
|
constraint={"target_width": 0.5, "tolerance": 0.05},
|
|
condition="NetClass == '50ohm'",
|
|
description="Precise width control for 50Ω traces"
|
|
)
|
|
])
|
|
|
|
return rf_rules
|
|
|
|
def create_automotive_rules(self) -> DRCRuleSet:
|
|
"""Create automotive-grade reliability rules."""
|
|
automotive_rules = DRCRuleSet(
|
|
name="Automotive",
|
|
version="1.0",
|
|
description="Automotive reliability and safety rules",
|
|
technology="Automotive"
|
|
)
|
|
|
|
automotive_rules.rules.extend([
|
|
DRCRule(
|
|
name="Safety Critical Clearance",
|
|
rule_type=RuleType.CLEARANCE,
|
|
severity=RuleSeverity.ERROR,
|
|
constraint={"min_clearance": 0.5},
|
|
condition="A.NetClass == 'Safety' or B.NetClass == 'Safety'",
|
|
description="Enhanced clearance for safety-critical circuits"
|
|
),
|
|
DRCRule(
|
|
name="Power Track Width",
|
|
rule_type=RuleType.TRACK_WIDTH,
|
|
severity=RuleSeverity.ERROR,
|
|
constraint={"min_width": 0.5},
|
|
condition="NetClass == 'Power'",
|
|
description="Minimum width for power distribution"
|
|
),
|
|
DRCRule(
|
|
name="Thermal Via Density",
|
|
rule_type=RuleType.VIA_SIZE,
|
|
severity=RuleSeverity.WARNING,
|
|
constraint={"min_density": 4}, # 4 vias per cm² for thermal
|
|
condition="Pad.ThermalPad == True",
|
|
description="Thermal via density for heat dissipation"
|
|
),
|
|
DRCRule(
|
|
name="Vibration Resistant Vias",
|
|
rule_type=RuleType.ANNULAR_RING,
|
|
severity=RuleSeverity.ERROR,
|
|
constraint={"min_annular_ring": 0.1},
|
|
description="Enhanced annular ring for vibration resistance"
|
|
)
|
|
])
|
|
|
|
return automotive_rules
|
|
|
|
def create_custom_rule(self, name: str, rule_type: RuleType,
|
|
constraint: dict[str, Any], severity: RuleSeverity = RuleSeverity.ERROR,
|
|
condition: str = None, description: str = None) -> DRCRule:
|
|
"""Create a custom DRC rule."""
|
|
return DRCRule(
|
|
name=name,
|
|
rule_type=rule_type,
|
|
severity=severity,
|
|
constraint=constraint,
|
|
condition=condition,
|
|
description=description
|
|
)
|
|
|
|
def validate_rule_syntax(self, rule: DRCRule) -> list[str]:
|
|
"""Validate rule syntax and return any errors."""
|
|
errors = []
|
|
|
|
# Validate constraint format
|
|
if rule.rule_type == RuleType.CLEARANCE:
|
|
if "min_clearance" not in rule.constraint:
|
|
errors.append("Clearance rule must specify min_clearance")
|
|
elif rule.constraint["min_clearance"] <= 0:
|
|
errors.append("Clearance must be positive")
|
|
|
|
elif rule.rule_type == RuleType.TRACK_WIDTH:
|
|
if "min_width" not in rule.constraint and "max_width" not in rule.constraint:
|
|
errors.append("Track width rule must specify min_width or max_width")
|
|
|
|
elif rule.rule_type == RuleType.VIA_SIZE:
|
|
if "min_drill" not in rule.constraint and "max_drill" not in rule.constraint:
|
|
errors.append("Via size rule must specify drill constraints")
|
|
|
|
# Validate condition syntax (basic check)
|
|
if rule.condition:
|
|
try:
|
|
# Basic syntax validation - could be more sophisticated
|
|
if not any(op in rule.condition for op in ["==", "!=", ">", "<", "intersects"]):
|
|
errors.append("Condition must contain a comparison operator")
|
|
except Exception as e:
|
|
errors.append(f"Invalid condition syntax: {e}")
|
|
|
|
return errors
|
|
|
|
def export_kicad_drc_rules(self, rule_set_name: str) -> str:
|
|
"""Export rule set as KiCad-compatible DRC rules."""
|
|
if rule_set_name not in self.rule_sets:
|
|
raise ValueError(f"Rule set '{rule_set_name}' not found")
|
|
|
|
rule_set = self.rule_sets[rule_set_name]
|
|
kicad_rules = []
|
|
|
|
kicad_rules.append(f"# DRC Rules: {rule_set.name}")
|
|
kicad_rules.append(f"# Description: {rule_set.description}")
|
|
kicad_rules.append(f"# Version: {rule_set.version}")
|
|
kicad_rules.append("")
|
|
|
|
for rule in rule_set.rules:
|
|
if not rule.enabled:
|
|
continue
|
|
|
|
kicad_rule = self._convert_to_kicad_rule(rule)
|
|
if kicad_rule:
|
|
kicad_rules.append(kicad_rule)
|
|
kicad_rules.append("")
|
|
|
|
return "\n".join(kicad_rules)
|
|
|
|
def _convert_to_kicad_rule(self, rule: DRCRule) -> str | None:
|
|
"""Convert DRC rule to KiCad rule format."""
|
|
try:
|
|
rule_lines = [f"# {rule.name}"]
|
|
if rule.description:
|
|
rule_lines.append(f"# {rule.description}")
|
|
|
|
if rule.rule_type == RuleType.CLEARANCE:
|
|
clearance = rule.constraint.get("min_clearance", 0.2)
|
|
rule_lines.append(f"(rule \"{rule.name}\"")
|
|
rule_lines.append(f" (constraint clearance (min {clearance}mm))")
|
|
if rule.condition:
|
|
rule_lines.append(f" (condition \"{rule.condition}\")")
|
|
rule_lines.append(")")
|
|
|
|
elif rule.rule_type == RuleType.TRACK_WIDTH:
|
|
if "min_width" in rule.constraint:
|
|
min_width = rule.constraint["min_width"]
|
|
rule_lines.append(f"(rule \"{rule.name}\"")
|
|
rule_lines.append(f" (constraint track_width (min {min_width}mm))")
|
|
if rule.condition:
|
|
rule_lines.append(f" (condition \"{rule.condition}\")")
|
|
rule_lines.append(")")
|
|
|
|
elif rule.rule_type == RuleType.VIA_SIZE:
|
|
rule_lines.append(f"(rule \"{rule.name}\"")
|
|
if "min_drill" in rule.constraint:
|
|
rule_lines.append(f" (constraint hole_size (min {rule.constraint['min_drill']}mm))")
|
|
if "max_drill" in rule.constraint:
|
|
rule_lines.append(f" (constraint hole_size (max {rule.constraint['max_drill']}mm))")
|
|
if rule.condition:
|
|
rule_lines.append(f" (condition \"{rule.condition}\")")
|
|
rule_lines.append(")")
|
|
|
|
return "\n".join(rule_lines)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to convert rule {rule.name}: {e}")
|
|
return None
|
|
|
|
def analyze_pcb_for_rule_violations(self, pcb_file_path: str,
|
|
rule_set_name: str = None) -> dict[str, Any]:
|
|
"""Analyze PCB file against rule set and report violations."""
|
|
if rule_set_name is None:
|
|
rule_set_name = self.active_rule_set
|
|
|
|
if rule_set_name not in self.rule_sets:
|
|
raise ValueError(f"Rule set '{rule_set_name}' not found")
|
|
|
|
rule_set = self.rule_sets[rule_set_name]
|
|
violations = []
|
|
|
|
# This would integrate with actual PCB analysis
|
|
# For now, return structure for potential violations
|
|
|
|
return {
|
|
"pcb_file": pcb_file_path,
|
|
"rule_set": rule_set_name,
|
|
"rule_count": len(rule_set.rules),
|
|
"violations": violations,
|
|
"summary": {
|
|
"errors": len([v for v in violations if v.get("severity") == "error"]),
|
|
"warnings": len([v for v in violations if v.get("severity") == "warning"]),
|
|
"total": len(violations)
|
|
}
|
|
}
|
|
|
|
def generate_manufacturing_constraints(self, technology: str = "standard") -> dict[str, Any]:
|
|
"""Generate manufacturing constraints for specific technology."""
|
|
constraints = {
|
|
"standard": {
|
|
"min_track_width": 0.1, # mm
|
|
"min_clearance": 0.2, # mm
|
|
"min_via_drill": 0.2, # mm
|
|
"min_annular_ring": 0.05, # mm
|
|
"aspect_ratio_limit": 8, # drill depth : diameter
|
|
"layer_count_limit": 16,
|
|
"board_thickness_range": [0.4, 6.0]
|
|
},
|
|
"hdi": {
|
|
"min_track_width": 0.075, # mm
|
|
"min_clearance": 0.075, # mm
|
|
"min_via_drill": 0.1, # mm
|
|
"min_annular_ring": 0.025, # mm
|
|
"aspect_ratio_limit": 1, # For microvias
|
|
"layer_count_limit": 20,
|
|
"board_thickness_range": [0.8, 3.2]
|
|
},
|
|
"rf": {
|
|
"min_track_width": 0.1, # mm
|
|
"min_clearance": 0.2, # mm
|
|
"impedance_tolerance": 5, # %
|
|
"via_stitching_max": 2.0, # mm spacing
|
|
"ground_plane_required": True,
|
|
"layer_symmetry_required": True
|
|
},
|
|
"automotive": {
|
|
"min_track_width": 0.15, # mm (more conservative)
|
|
"min_clearance": 0.3, # mm (enhanced reliability)
|
|
"min_via_drill": 0.25, # mm
|
|
"min_annular_ring": 0.075, # mm
|
|
"temperature_range": [-40, 125], # °C
|
|
"vibration_resistant": True
|
|
}
|
|
}
|
|
|
|
return constraints.get(technology, constraints["standard"])
|
|
|
|
def add_rule_set(self, rule_set: DRCRuleSet) -> None:
|
|
"""Add a rule set to the manager."""
|
|
self.rule_sets[rule_set.name.lower().replace(" ", "_")] = rule_set
|
|
|
|
def get_rule_set_names(self) -> list[str]:
|
|
"""Get list of available rule set names."""
|
|
return list(self.rule_sets.keys())
|
|
|
|
def set_active_rule_set(self, name: str) -> None:
|
|
"""Set the active rule set."""
|
|
if name not in self.rule_sets:
|
|
raise ValueError(f"Rule set '{name}' not found")
|
|
self.active_rule_set = name
|
|
|
|
|
|
def create_drc_manager() -> AdvancedDRCManager:
|
|
"""Create and initialize a DRC manager with default rule sets."""
|
|
manager = AdvancedDRCManager()
|
|
|
|
# Add specialized rule sets
|
|
manager.add_rule_set(manager.create_high_density_rules())
|
|
manager.add_rule_set(manager.create_rf_rules())
|
|
manager.add_rule_set(manager.create_automotive_rules())
|
|
|
|
return manager
|