kicad-mcp/kicad_mcp/utils/netlist_parser.py

449 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