- Implement 3D model analysis and mechanical constraints checking - Add advanced DRC rule customization for HDI, RF, and automotive applications - Create symbol library management with analysis and validation tools - Implement PCB layer stack-up analysis with impedance calculations - Fix Context parameter validation errors causing client failures - Add enhanced tool annotations with examples for better LLM compatibility - Include comprehensive test coverage improvements (22.21% coverage) - Add CLAUDE.md documentation for development guidance New Advanced Tools: • 3D model analysis: analyze_3d_models, check_mechanical_constraints • Advanced DRC: create_drc_rule_set, analyze_pcb_drc_violations • Symbol management: analyze_symbol_library, validate_symbol_library • Layer analysis: analyze_pcb_stackup, calculate_trace_impedance 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
469 lines
16 KiB
Python
469 lines
16 KiB
Python
"""
|
|
KiCad schematic netlist extraction utilities.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
from typing import Any, Dict, List
|
|
from collections import defaultdict
|
|
|
|
|
|
class SchematicParser:
|
|
"""Parser for KiCad schematic files to extract netlist information."""
|
|
|
|
def __init__(self, schematic_path: str):
|
|
"""Initialize the schematic parser.
|
|
|
|
Args:
|
|
schematic_path: Path to the KiCad schematic file (.kicad_sch)
|
|
"""
|
|
self.schematic_path = schematic_path
|
|
self.content = ""
|
|
self.components = []
|
|
self.labels = []
|
|
self.wires = []
|
|
self.junctions = []
|
|
self.no_connects = []
|
|
self.power_symbols = []
|
|
self.hierarchical_labels = []
|
|
self.global_labels = []
|
|
|
|
# Netlist information
|
|
self.nets = defaultdict(list) # Net name -> connected pins
|
|
self.component_pins = {} # (component_ref, pin_num) -> net_name
|
|
|
|
# Component information
|
|
self.component_info = {} # component_ref -> component details
|
|
|
|
# Load the file
|
|
self._load_schematic()
|
|
|
|
def _load_schematic(self) -> None:
|
|
"""Load the schematic file content."""
|
|
if not os.path.exists(self.schematic_path):
|
|
print(f"Schematic file not found: {self.schematic_path}")
|
|
raise FileNotFoundError(f"Schematic file not found: {self.schematic_path}")
|
|
|
|
try:
|
|
with open(self.schematic_path, "r") as f:
|
|
self.content = f.read()
|
|
print(f"Successfully loaded schematic: {self.schematic_path}")
|
|
except Exception as e:
|
|
print(f"Error reading schematic file: {str(e)}")
|
|
raise
|
|
|
|
def parse(self) -> Dict[str, Any]:
|
|
"""Parse the schematic to extract netlist information.
|
|
|
|
Returns:
|
|
Dictionary with parsed netlist information
|
|
"""
|
|
print("Starting schematic parsing")
|
|
|
|
# Extract symbols (components)
|
|
self._extract_components()
|
|
|
|
# Extract wires
|
|
self._extract_wires()
|
|
|
|
# Extract junctions
|
|
self._extract_junctions()
|
|
|
|
# Extract labels
|
|
self._extract_labels()
|
|
|
|
# Extract power symbols
|
|
self._extract_power_symbols()
|
|
|
|
# Extract no-connects
|
|
self._extract_no_connects()
|
|
|
|
# Build netlist
|
|
self._build_netlist()
|
|
|
|
# Create result
|
|
result = {
|
|
"components": self.component_info,
|
|
"nets": dict(self.nets),
|
|
"labels": self.labels,
|
|
"wires": self.wires,
|
|
"junctions": self.junctions,
|
|
"power_symbols": self.power_symbols,
|
|
"component_count": len(self.component_info),
|
|
"net_count": len(self.nets),
|
|
}
|
|
|
|
print(
|
|
f"Schematic parsing complete: found {len(self.component_info)} components and {len(self.nets)} nets"
|
|
)
|
|
return result
|
|
|
|
def _extract_s_expressions(self, pattern: str) -> List[str]:
|
|
"""Extract all matching S-expressions from the schematic content.
|
|
|
|
Args:
|
|
pattern: Regex pattern to match the start of S-expressions
|
|
|
|
Returns:
|
|
List of matching S-expressions
|
|
"""
|
|
matches = []
|
|
positions = []
|
|
|
|
# Find all starting positions of matches
|
|
for match in re.finditer(pattern, self.content):
|
|
positions.append(match.start())
|
|
|
|
# Extract full S-expressions for each match
|
|
for pos in positions:
|
|
# Start from the matching position
|
|
current_pos = pos
|
|
depth = 0
|
|
s_exp = ""
|
|
|
|
# Extract the full S-expression by tracking parentheses
|
|
while current_pos < len(self.content):
|
|
char = self.content[current_pos]
|
|
s_exp += char
|
|
|
|
if char == "(":
|
|
depth += 1
|
|
elif char == ")":
|
|
depth -= 1
|
|
if depth == 0:
|
|
# Found the end of the S-expression
|
|
break
|
|
|
|
current_pos += 1
|
|
|
|
matches.append(s_exp)
|
|
|
|
return matches
|
|
|
|
def _extract_components(self) -> None:
|
|
"""Extract component information from schematic."""
|
|
print("Extracting components")
|
|
|
|
# Extract all symbol expressions (components)
|
|
symbols = self._extract_s_expressions(r"\(symbol\s+")
|
|
|
|
for symbol in symbols:
|
|
component = self._parse_component(symbol)
|
|
if component:
|
|
self.components.append(component)
|
|
|
|
# Add to component info dictionary
|
|
ref = component.get("reference", "Unknown")
|
|
self.component_info[ref] = component
|
|
|
|
print(f"Extracted {len(self.components)} components")
|
|
|
|
def _parse_component(self, symbol_expr: str) -> Dict[str, Any]:
|
|
"""Parse a component from a symbol S-expression.
|
|
|
|
Args:
|
|
symbol_expr: Symbol S-expression
|
|
|
|
Returns:
|
|
Component information dictionary
|
|
"""
|
|
component = {}
|
|
|
|
# Extract library component ID
|
|
lib_id_match = re.search(r'\(lib_id\s+"([^"]+)"\)', symbol_expr)
|
|
if lib_id_match:
|
|
component["lib_id"] = lib_id_match.group(1)
|
|
|
|
# Extract reference (e.g., R1, C2)
|
|
property_matches = re.finditer(r'\(property\s+"([^"]+)"\s+"([^"]+)"', symbol_expr)
|
|
for match in property_matches:
|
|
prop_name = match.group(1)
|
|
prop_value = match.group(2)
|
|
|
|
if prop_name == "Reference":
|
|
component["reference"] = prop_value
|
|
elif prop_name == "Value":
|
|
component["value"] = prop_value
|
|
elif prop_name == "Footprint":
|
|
component["footprint"] = prop_value
|
|
else:
|
|
# Store other properties
|
|
if "properties" not in component:
|
|
component["properties"] = {}
|
|
component["properties"][prop_name] = prop_value
|
|
|
|
# Extract position
|
|
pos_match = re.search(r"\(at\s+([\d\.-]+)\s+([\d\.-]+)(\s+[\d\.-]+)?\)", symbol_expr)
|
|
if pos_match:
|
|
component["position"] = {
|
|
"x": float(pos_match.group(1)),
|
|
"y": float(pos_match.group(2)),
|
|
"angle": float(pos_match.group(3).strip() if pos_match.group(3) else 0),
|
|
}
|
|
|
|
# Extract pins
|
|
pins = []
|
|
pin_matches = re.finditer(
|
|
r'\(pin\s+\(num\s+"([^"]+)"\)\s+\(name\s+"([^"]+)"\)', symbol_expr
|
|
)
|
|
for match in pin_matches:
|
|
pin_num = match.group(1)
|
|
pin_name = match.group(2)
|
|
pins.append({"num": pin_num, "name": pin_name})
|
|
|
|
if pins:
|
|
component["pins"] = pins
|
|
|
|
return component
|
|
|
|
def _extract_wires(self) -> None:
|
|
"""Extract wire information from schematic."""
|
|
print("Extracting wires")
|
|
|
|
# Extract all wire expressions
|
|
wires = self._extract_s_expressions(r"\(wire\s+")
|
|
|
|
for wire in wires:
|
|
# Extract the wire coordinates
|
|
pts_match = re.search(
|
|
r"\(pts\s+\(xy\s+([\d\.-]+)\s+([\d\.-]+)\)\s+\(xy\s+([\d\.-]+)\s+([\d\.-]+)\)\)",
|
|
wire,
|
|
)
|
|
if pts_match:
|
|
self.wires.append(
|
|
{
|
|
"start": {"x": float(pts_match.group(1)), "y": float(pts_match.group(2))},
|
|
"end": {"x": float(pts_match.group(3)), "y": float(pts_match.group(4))},
|
|
}
|
|
)
|
|
|
|
print(f"Extracted {len(self.wires)} wires")
|
|
|
|
def _extract_junctions(self) -> None:
|
|
"""Extract junction information from schematic."""
|
|
print("Extracting junctions")
|
|
|
|
# Extract all junction expressions
|
|
junctions = self._extract_s_expressions(r"\(junction\s+")
|
|
|
|
for junction in junctions:
|
|
# Extract the junction coordinates
|
|
xy_match = re.search(r"\(junction\s+\(xy\s+([\d\.-]+)\s+([\d\.-]+)\)\)", junction)
|
|
if xy_match:
|
|
self.junctions.append(
|
|
{"x": float(xy_match.group(1)), "y": float(xy_match.group(2))}
|
|
)
|
|
|
|
print(f"Extracted {len(self.junctions)} junctions")
|
|
|
|
def _extract_labels(self) -> None:
|
|
"""Extract label information from schematic."""
|
|
print("Extracting labels")
|
|
|
|
# Extract local labels
|
|
local_labels = self._extract_s_expressions(r"\(label\s+")
|
|
|
|
for label in local_labels:
|
|
# Extract label text and position
|
|
label_match = re.search(
|
|
r'\(label\s+"([^"]+)"\s+\(at\s+([\d\.-]+)\s+([\d\.-]+)(\s+[\d\.-]+)?\)', label
|
|
)
|
|
if label_match:
|
|
self.labels.append(
|
|
{
|
|
"type": "local",
|
|
"text": label_match.group(1),
|
|
"position": {
|
|
"x": float(label_match.group(2)),
|
|
"y": float(label_match.group(3)),
|
|
"angle": float(
|
|
label_match.group(4).strip() if label_match.group(4) else 0
|
|
),
|
|
},
|
|
}
|
|
)
|
|
|
|
# Extract global labels
|
|
global_labels = self._extract_s_expressions(r"\(global_label\s+")
|
|
|
|
for label in global_labels:
|
|
# Extract global label text and position
|
|
label_match = re.search(
|
|
r'\(global_label\s+"([^"]+)"\s+\(shape\s+([^\s\)]+)\)\s+\(at\s+([\d\.-]+)\s+([\d\.-]+)(\s+[\d\.-]+)?\)',
|
|
label,
|
|
)
|
|
if label_match:
|
|
self.global_labels.append(
|
|
{
|
|
"type": "global",
|
|
"text": label_match.group(1),
|
|
"shape": label_match.group(2),
|
|
"position": {
|
|
"x": float(label_match.group(3)),
|
|
"y": float(label_match.group(4)),
|
|
"angle": float(
|
|
label_match.group(5).strip() if label_match.group(5) else 0
|
|
),
|
|
},
|
|
}
|
|
)
|
|
|
|
# Extract hierarchical labels
|
|
hierarchical_labels = self._extract_s_expressions(r"\(hierarchical_label\s+")
|
|
|
|
for label in hierarchical_labels:
|
|
# Extract hierarchical label text and position
|
|
label_match = re.search(
|
|
r'\(hierarchical_label\s+"([^"]+)"\s+\(shape\s+([^\s\)]+)\)\s+\(at\s+([\d\.-]+)\s+([\d\.-]+)(\s+[\d\.-]+)?\)',
|
|
label,
|
|
)
|
|
if label_match:
|
|
self.hierarchical_labels.append(
|
|
{
|
|
"type": "hierarchical",
|
|
"text": label_match.group(1),
|
|
"shape": label_match.group(2),
|
|
"position": {
|
|
"x": float(label_match.group(3)),
|
|
"y": float(label_match.group(4)),
|
|
"angle": float(
|
|
label_match.group(5).strip() if label_match.group(5) else 0
|
|
),
|
|
},
|
|
}
|
|
)
|
|
|
|
print(
|
|
f"Extracted {len(self.labels)} local labels, {len(self.global_labels)} global labels, and {len(self.hierarchical_labels)} hierarchical labels"
|
|
)
|
|
|
|
def _extract_power_symbols(self) -> None:
|
|
"""Extract power symbol information from schematic."""
|
|
print("Extracting power symbols")
|
|
|
|
# Extract all power symbol expressions
|
|
power_symbols = self._extract_s_expressions(r'\(symbol\s+\(lib_id\s+"power:')
|
|
|
|
for symbol in power_symbols:
|
|
# Extract power symbol type and position
|
|
type_match = re.search(r'\(lib_id\s+"power:([^"]+)"\)', symbol)
|
|
pos_match = re.search(r"\(at\s+([\d\.-]+)\s+([\d\.-]+)(\s+[\d\.-]+)?\)", symbol)
|
|
|
|
if type_match and pos_match:
|
|
self.power_symbols.append(
|
|
{
|
|
"type": type_match.group(1),
|
|
"position": {
|
|
"x": float(pos_match.group(1)),
|
|
"y": float(pos_match.group(2)),
|
|
"angle": float(pos_match.group(3).strip() if pos_match.group(3) else 0),
|
|
},
|
|
}
|
|
)
|
|
|
|
print(f"Extracted {len(self.power_symbols)} power symbols")
|
|
|
|
def _extract_no_connects(self) -> None:
|
|
"""Extract no-connect information from schematic."""
|
|
print("Extracting no-connects")
|
|
|
|
# Extract all no-connect expressions
|
|
no_connects = self._extract_s_expressions(r"\(no_connect\s+")
|
|
|
|
for no_connect in no_connects:
|
|
# Extract the no-connect coordinates
|
|
xy_match = re.search(r"\(no_connect\s+\(at\s+([\d\.-]+)\s+([\d\.-]+)\)", no_connect)
|
|
if xy_match:
|
|
self.no_connects.append(
|
|
{"x": float(xy_match.group(1)), "y": float(xy_match.group(2))}
|
|
)
|
|
|
|
print(f"Extracted {len(self.no_connects)} no-connects")
|
|
|
|
def _build_netlist(self) -> None:
|
|
"""Build the netlist from extracted components and connections."""
|
|
print("Building netlist from schematic data")
|
|
|
|
# TODO: Implement netlist building algorithm
|
|
# This is a complex task that involves:
|
|
# 1. Tracking connections between components via wires
|
|
# 2. Handling labels (local, global, hierarchical)
|
|
# 3. Processing power symbols
|
|
# 4. Resolving junctions
|
|
|
|
# For now, we'll implement a basic version that creates a list of nets
|
|
# based on component references and pin numbers
|
|
|
|
# Process global labels as nets
|
|
for label in self.global_labels:
|
|
net_name = label["text"]
|
|
self.nets[net_name] = [] # Initialize empty list for this net
|
|
|
|
# Process power symbols as nets
|
|
for power in self.power_symbols:
|
|
net_name = power["type"]
|
|
if net_name not in self.nets:
|
|
self.nets[net_name] = []
|
|
|
|
# In a full implementation, we would now trace connections between
|
|
# components, but that requires a more complex algorithm to follow wires
|
|
# and detect connected pins
|
|
|
|
# For demonstration, we'll add a placeholder note
|
|
print("Note: Full netlist building requires complex connectivity tracing")
|
|
print(f"Found {len(self.nets)} potential nets from labels and power symbols")
|
|
|
|
|
|
def extract_netlist(schematic_path: str) -> Dict[str, Any]:
|
|
"""Extract netlist information from a KiCad schematic file.
|
|
|
|
Args:
|
|
schematic_path: Path to the KiCad schematic file (.kicad_sch)
|
|
|
|
Returns:
|
|
Dictionary with netlist information
|
|
"""
|
|
try:
|
|
parser = SchematicParser(schematic_path)
|
|
return parser.parse()
|
|
except Exception as e:
|
|
print(f"Error extracting netlist: {str(e)}")
|
|
return {"error": str(e), "components": {}, "nets": {}, "component_count": 0, "net_count": 0}
|
|
|
|
|
|
def analyze_netlist(netlist_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Analyze netlist data to provide insights.
|
|
|
|
Args:
|
|
netlist_data: Dictionary with netlist information
|
|
|
|
Returns:
|
|
Dictionary with analysis results
|
|
"""
|
|
results = {
|
|
"component_count": netlist_data.get("component_count", 0),
|
|
"net_count": netlist_data.get("net_count", 0),
|
|
"component_types": defaultdict(int),
|
|
"power_nets": [],
|
|
}
|
|
|
|
# Analyze component types
|
|
for ref, component in netlist_data.get("components", {}).items():
|
|
# Extract component type from reference (e.g., R1 -> R)
|
|
comp_type = re.match(r"^([A-Za-z_]+)", ref)
|
|
if comp_type:
|
|
results["component_types"][comp_type.group(1)] += 1
|
|
|
|
# Identify power nets
|
|
for net_name in netlist_data.get("nets", {}):
|
|
if any(
|
|
net_name.startswith(prefix) for prefix in ["VCC", "VDD", "GND", "+5V", "+3V3", "+12V"]
|
|
):
|
|
results["power_nets"].append(net_name)
|
|
|
|
# Count pin connections
|
|
total_pins = sum(len(pins) for pins in netlist_data.get("nets", {}).values())
|
|
results["total_pin_connections"] = total_pins
|
|
|
|
return results
|