kicad-mcp/kicad_mcp/utils/component_utils.py
Ryan Malloy bc0f3db97c
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
Implement comprehensive AI/LLM integration for KiCad MCP server
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>
2025-08-11 16:15:58 -06:00

583 lines
19 KiB
Python

"""
Utility functions for working with KiCad component values and properties.
"""
from enum import Enum
import re
from typing import Any
class ComponentType(Enum):
"""Enumeration of electronic component types."""
RESISTOR = "resistor"
CAPACITOR = "capacitor"
INDUCTOR = "inductor"
DIODE = "diode"
TRANSISTOR = "transistor"
IC = "integrated_circuit"
CONNECTOR = "connector"
CRYSTAL = "crystal"
VOLTAGE_REGULATOR = "voltage_regulator"
FUSE = "fuse"
SWITCH = "switch"
RELAY = "relay"
TRANSFORMER = "transformer"
LED = "led"
UNKNOWN = "unknown"
def extract_voltage_from_regulator(value: str) -> str:
"""Extract output voltage from a voltage regulator part number or description.
Args:
value: Regulator part number or description
Returns:
Extracted voltage as a string or "unknown" if not found
"""
# Common patterns:
# 78xx/79xx series: 7805 = 5V, 7812 = 12V
# LDOs often have voltage in the part number, like LM1117-3.3
# 78xx/79xx series
match = re.search(r"78(\d\d)|79(\d\d)", value, re.IGNORECASE)
if match:
group = match.group(1) or match.group(2)
# Convert code to voltage (e.g., 05 -> 5V, 12 -> 12V)
try:
voltage = int(group)
# For 78xx series, voltage code is directly in volts
if voltage < 50: # Sanity check to prevent weird values
return f"{voltage}V"
except ValueError:
pass
# Look for common voltage indicators in the string
voltage_patterns = [
r"(\d+\.?\d*)V", # 3.3V, 5V, etc.
r"-(\d+\.?\d*)V", # -5V, -12V, etc. (for negative regulators)
r"(\d+\.?\d*)[_-]?V", # 3.3_V, 5-V, etc.
r"[_-](\d+\.?\d*)", # LM1117-3.3, LD1117-3.3, etc.
]
for pattern in voltage_patterns:
match = re.search(pattern, value, re.IGNORECASE)
if match:
try:
voltage = float(match.group(1))
if 0 < voltage < 50: # Sanity check
# Format as integer if it's a whole number
if voltage.is_integer():
return f"{int(voltage)}V"
else:
return f"{voltage}V"
except ValueError:
pass
# Check for common fixed voltage regulators
regulators = {
"LM7805": "5V",
"LM7809": "9V",
"LM7812": "12V",
"LM7905": "-5V",
"LM7912": "-12V",
"LM1117-3.3": "3.3V",
"LM1117-5": "5V",
"LM317": "Adjustable",
"LM337": "Adjustable (Negative)",
"AP1117-3.3": "3.3V",
"AMS1117-3.3": "3.3V",
"L7805": "5V",
"L7812": "12V",
"MCP1700-3.3": "3.3V",
"MCP1700-5.0": "5V",
}
for reg, volt in regulators.items():
if re.search(re.escape(reg), value, re.IGNORECASE):
return volt
return "unknown"
def extract_frequency_from_value(value: str) -> str:
"""Extract frequency information from a component value or description.
Args:
value: Component value or description (e.g., "16MHz", "Crystal 8MHz")
Returns:
Frequency as a string or "unknown" if not found
"""
# Common frequency patterns with various units
frequency_patterns = [
r"(\d+\.?\d*)[\s-]*([kKmMgG]?)[hH][zZ]", # 16MHz, 32.768 kHz, etc.
r"(\d+\.?\d*)[\s-]*([kKmMgG])", # 16M, 32.768k, etc.
]
for pattern in frequency_patterns:
match = re.search(pattern, value, re.IGNORECASE)
if match:
try:
freq = float(match.group(1))
unit = match.group(2).upper() if match.group(2) else ""
# Make sure the frequency is in a reasonable range
if freq > 0:
# Format the output
if unit == "K":
if freq >= 1000:
return f"{freq / 1000:.3f}MHz"
else:
return f"{freq:.3f}kHz"
elif unit == "M":
if freq >= 1000:
return f"{freq / 1000:.3f}GHz"
else:
return f"{freq:.3f}MHz"
elif unit == "G":
return f"{freq:.3f}GHz"
else: # No unit, need to determine based on value
if freq < 1000:
return f"{freq:.3f}Hz"
elif freq < 1000000:
return f"{freq / 1000:.3f}kHz"
elif freq < 1000000000:
return f"{freq / 1000000:.3f}MHz"
else:
return f"{freq / 1000000000:.3f}GHz"
except ValueError:
pass
# Check for common crystal frequencies
if "32.768" in value or "32768" in value:
return "32.768kHz" # Common RTC crystal
elif "16M" in value or "16MHZ" in value.upper():
return "16MHz" # Common MCU crystal
elif "8M" in value or "8MHZ" in value.upper():
return "8MHz"
elif "20M" in value or "20MHZ" in value.upper():
return "20MHz"
elif "27M" in value or "27MHZ" in value.upper():
return "27MHz"
elif "25M" in value or "25MHZ" in value.upper():
return "25MHz"
return "unknown"
def extract_resistance_value(value: str) -> tuple[float | None, str | None]:
"""Extract resistance value and unit from component value.
Args:
value: Resistance value (e.g., "10k", "4.7k", "100")
Returns:
Tuple of (numeric value, unit) or (None, None) if parsing fails
"""
# Common resistance patterns
# 10k, 4.7k, 100R, 1M, 10, etc.
match = re.search(r"(\d+\.?\d*)([kKmMrRΩ]?)", value)
if match:
try:
resistance = float(match.group(1))
unit = match.group(2).upper() if match.group(2) else "Ω"
# Normalize unit
if unit == "R" or unit == "":
unit = "Ω"
return resistance, unit
except ValueError:
pass
# Handle special case like "4k7" (means 4.7k)
match = re.search(r"(\d+)[kKmM](\d+)", value)
if match:
try:
value1 = int(match.group(1))
value2 = int(match.group(2))
resistance = float(f"{value1}.{value2}")
unit = "k" if "k" in value.lower() else "M" if "m" in value.lower() else "Ω"
return resistance, unit
except ValueError:
pass
return None, None
def extract_capacitance_value(value: str) -> tuple[float | None, str | None]:
"""Extract capacitance value and unit from component value.
Args:
value: Capacitance value (e.g., "10uF", "4.7nF", "100pF")
Returns:
Tuple of (numeric value, unit) or (None, None) if parsing fails
"""
# Common capacitance patterns
# 10uF, 4.7nF, 100pF, etc.
match = re.search(r"(\d+\.?\d*)([pPnNuUμF]+)", value)
if match:
try:
capacitance = float(match.group(1))
unit = match.group(2).lower()
# Normalize unit
if "p" in unit or "pf" in unit:
unit = "pF"
elif "n" in unit or "nf" in unit:
unit = "nF"
elif "u" in unit or "μ" in unit or "uf" in unit or "μf" in unit:
unit = "μF"
else:
unit = "F"
return capacitance, unit
except ValueError:
pass
# Handle special case like "4n7" (means 4.7nF)
match = re.search(r"(\d+)[pPnNuUμ](\d+)", value)
if match:
try:
value1 = int(match.group(1))
value2 = int(match.group(2))
capacitance = float(f"{value1}.{value2}")
if "p" in value.lower():
unit = "pF"
elif "n" in value.lower():
unit = "nF"
elif "u" in value.lower() or "μ" in value:
unit = "μF"
else:
unit = "F"
return capacitance, unit
except ValueError:
pass
return None, None
def extract_inductance_value(value: str) -> tuple[float | None, str | None]:
"""Extract inductance value and unit from component value.
Args:
value: Inductance value (e.g., "10uH", "4.7nH", "100mH")
Returns:
Tuple of (numeric value, unit) or (None, None) if parsing fails
"""
# Common inductance patterns
# 10uH, 4.7nH, 100mH, etc.
match = re.search(r"(\d+\.?\d*)([pPnNuUμmM][hH])", value)
if match:
try:
inductance = float(match.group(1))
unit = match.group(2).lower()
# Normalize unit
if "p" in unit:
unit = "pH"
elif "n" in unit:
unit = "nH"
elif "u" in unit or "μ" in unit:
unit = "μH"
elif "m" in unit:
unit = "mH"
else:
unit = "H"
return inductance, unit
except ValueError:
pass
# Handle special case like "4u7" (means 4.7uH)
match = re.search(r"(\d+)[pPnNuUμmM](\d+)[hH]", value)
if match:
try:
value1 = int(match.group(1))
value2 = int(match.group(2))
inductance = float(f"{value1}.{value2}")
if "p" in value.lower():
unit = "pH"
elif "n" in value.lower():
unit = "nH"
elif "u" in value.lower() or "μ" in value:
unit = "μH"
elif "m" in value.lower():
unit = "mH"
else:
unit = "H"
return inductance, unit
except ValueError:
pass
return None, None
def format_resistance(resistance: float, unit: str) -> str:
"""Format resistance value with appropriate unit.
Args:
resistance: Resistance value
unit: Unit string (Ω, k, M)
Returns:
Formatted resistance string
"""
if unit == "Ω":
return f"{resistance:.0f}Ω" if resistance.is_integer() else f"{resistance}Ω"
elif unit == "k":
return f"{resistance:.0f}" if resistance.is_integer() else f"{resistance}"
elif unit == "M":
return f"{resistance:.0f}" if resistance.is_integer() else f"{resistance}"
else:
return f"{resistance}{unit}"
def format_capacitance(capacitance: float, unit: str) -> str:
"""Format capacitance value with appropriate unit.
Args:
capacitance: Capacitance value
unit: Unit string (pF, nF, μF, F)
Returns:
Formatted capacitance string
"""
if capacitance.is_integer():
return f"{int(capacitance)}{unit}"
else:
return f"{capacitance}{unit}"
def format_inductance(inductance: float, unit: str) -> str:
"""Format inductance value with appropriate unit.
Args:
inductance: Inductance value
unit: Unit string (pH, nH, μH, mH, H)
Returns:
Formatted inductance string
"""
if inductance.is_integer():
return f"{int(inductance)}{unit}"
else:
return f"{inductance}{unit}"
def normalize_component_value(value: str, component_type: str) -> str:
"""Normalize a component value string based on component type.
Args:
value: Raw component value string
component_type: Type of component (R, C, L, etc.)
Returns:
Normalized value string
"""
if component_type == "R":
resistance, unit = extract_resistance_value(value)
if resistance is not None and unit is not None:
return format_resistance(resistance, unit)
elif component_type == "C":
capacitance, unit = extract_capacitance_value(value)
if capacitance is not None and unit is not None:
return format_capacitance(capacitance, unit)
elif component_type == "L":
inductance, unit = extract_inductance_value(value)
if inductance is not None and unit is not None:
return format_inductance(inductance, unit)
# For other component types or if parsing fails, return the original value
return value
def get_component_type_from_reference(reference: str) -> str:
"""Determine component type from reference designator.
Args:
reference: Component reference (e.g., R1, C2, U3)
Returns:
Component type letter (R, C, L, Q, etc.)
"""
# Extract the alphabetic prefix (component type)
match = re.match(r"^([A-Za-z_]+)", reference)
if match:
return match.group(1)
return ""
def is_power_component(component: dict[str, Any]) -> bool:
"""Check if a component is likely a power-related component.
Args:
component: Component information dictionary
Returns:
True if the component is power-related, False otherwise
"""
ref = component.get("reference", "")
value = component.get("value", "").upper()
lib_id = component.get("lib_id", "").upper()
# Check reference designator
if ref.startswith(("VR", "PS", "REG")):
return True
# Check for power-related terms in value or library ID
power_terms = ["VCC", "VDD", "GND", "POWER", "PWR", "SUPPLY", "REGULATOR", "LDO"]
if any(term in value or term in lib_id for term in power_terms):
return True
# Check for regulator part numbers
regulator_patterns = [
r"78\d\d", # 7805, 7812, etc.
r"79\d\d", # 7905, 7912, etc.
r"LM\d{3}", # LM317, LM337, etc.
r"LM\d{4}", # LM1117, etc.
r"AMS\d{4}", # AMS1117, etc.
r"MCP\d{4}", # MCP1700, etc.
]
if any(re.search(pattern, value, re.IGNORECASE) for pattern in regulator_patterns):
return True
# Not identified as a power component
return False
def get_component_type(value: str) -> ComponentType:
"""Determine component type from value string.
Args:
value: Component value or part number
Returns:
ComponentType enum value
"""
value_lower = value.lower()
# Check for resistor patterns
if (re.search(r'\d+[kmgr]?ω|ω', value_lower) or
re.search(r'\d+[kmgr]?ohm', value_lower) or
re.search(r'resistor', value_lower)):
return ComponentType.RESISTOR
# Check for capacitor patterns
if (re.search(r'\d+[pnumkμ]?f', value_lower) or
re.search(r'capacitor|cap', value_lower)):
return ComponentType.CAPACITOR
# Check for inductor patterns
if (re.search(r'\d+[pnumkμ]?h', value_lower) or
re.search(r'inductor|coil', value_lower)):
return ComponentType.INDUCTOR
# Check for diode patterns
if ('diode' in value_lower or 'led' in value_lower or
value_lower.startswith(('1n', 'bar', 'ss'))):
if 'led' in value_lower:
return ComponentType.LED
return ComponentType.DIODE
# Check for transistor patterns
if (re.search(r'transistor|mosfet|bjt|fet', value_lower) or
value_lower.startswith(('2n', 'bc', 'tip', 'irf', 'fqp'))):
return ComponentType.TRANSISTOR
# Check for IC patterns
if (re.search(r'ic|chip|processor|mcu|cpu', value_lower) or
value_lower.startswith(('lm', 'tlv', 'op', 'ad', 'max', 'lt'))):
return ComponentType.IC
# Check for voltage regulator patterns
if (re.search(r'regulator|ldo', value_lower) or
re.search(r'78\d\d|79\d\d|lm317|ams1117', value_lower)):
return ComponentType.VOLTAGE_REGULATOR
# Check for connector patterns
if re.search(r'connector|conn|jack|plug|header', value_lower):
return ComponentType.CONNECTOR
# Check for crystal patterns
if re.search(r'crystal|xtal|oscillator|mhz|khz', value_lower):
return ComponentType.CRYSTAL
# Check for fuse patterns
if re.search(r'fuse|ptc', value_lower):
return ComponentType.FUSE
# Check for switch patterns
if re.search(r'switch|button|sw', value_lower):
return ComponentType.SWITCH
# Check for relay patterns
if re.search(r'relay', value_lower):
return ComponentType.RELAY
# Check for transformer patterns
if re.search(r'transformer|trans', value_lower):
return ComponentType.TRANSFORMER
return ComponentType.UNKNOWN
def get_standard_values(component_type: ComponentType) -> list[str]:
"""Get standard component values for a given component type.
Args:
component_type: Type of component
Returns:
List of standard values as strings
"""
if component_type == ComponentType.RESISTOR:
return [
"", "1.2Ω", "1.5Ω", "1.8Ω", "2.2Ω", "2.7Ω", "3.3Ω", "3.9Ω", "4.7Ω", "5.6Ω", "6.8Ω", "8.2Ω",
"10Ω", "12Ω", "15Ω", "18Ω", "22Ω", "27Ω", "33Ω", "39Ω", "47Ω", "56Ω", "68Ω", "82Ω",
"100Ω", "120Ω", "150Ω", "180Ω", "220Ω", "270Ω", "330Ω", "390Ω", "470Ω", "560Ω", "680Ω", "820Ω",
"1kΩ", "1.2kΩ", "1.5kΩ", "1.8kΩ", "2.2kΩ", "2.7kΩ", "3.3kΩ", "3.9kΩ", "4.7kΩ", "5.6kΩ", "6.8kΩ", "8.2kΩ",
"10kΩ", "12kΩ", "15kΩ", "18kΩ", "22kΩ", "27kΩ", "33kΩ", "39kΩ", "47kΩ", "56kΩ", "68kΩ", "82kΩ",
"100kΩ", "120kΩ", "150kΩ", "180kΩ", "220kΩ", "270kΩ", "330kΩ", "390kΩ", "470kΩ", "560kΩ", "680kΩ", "820kΩ",
"1MΩ", "1.2MΩ", "1.5MΩ", "1.8MΩ", "2.2MΩ", "2.7MΩ", "3.3MΩ", "3.9MΩ", "4.7MΩ", "5.6MΩ", "6.8MΩ", "8.2MΩ",
"10MΩ"
]
elif component_type == ComponentType.CAPACITOR:
return [
"1pF", "1.5pF", "2.2pF", "3.3pF", "4.7pF", "6.8pF", "10pF", "15pF", "22pF", "33pF", "47pF", "68pF",
"100pF", "150pF", "220pF", "330pF", "470pF", "680pF",
"1nF", "1.5nF", "2.2nF", "3.3nF", "4.7nF", "6.8nF", "10nF", "15nF", "22nF", "33nF", "47nF", "68nF",
"100nF", "150nF", "220nF", "330nF", "470nF", "680nF",
"1μF", "1.5μF", "2.2μF", "3.3μF", "4.7μF", "6.8μF", "10μF", "15μF", "22μF", "33μF", "47μF", "68μF",
"100μF", "150μF", "220μF", "330μF", "470μF", "680μF",
"1000μF", "1500μF", "2200μF", "3300μF", "4700μF", "6800μF", "10000μF"
]
elif component_type == ComponentType.INDUCTOR:
return [
"1nH", "1.5nH", "2.2nH", "3.3nH", "4.7nH", "6.8nH", "10nH", "15nH", "22nH", "33nH", "47nH", "68nH",
"100nH", "150nH", "220nH", "330nH", "470nH", "680nH",
"1μH", "1.5μH", "2.2μH", "3.3μH", "4.7μH", "6.8μH", "10μH", "15μH", "22μH", "33μH", "47μH", "68μH",
"100μH", "150μH", "220μH", "330μH", "470μH", "680μH",
"1mH", "1.5mH", "2.2mH", "3.3mH", "4.7mH", "6.8mH", "10mH", "15mH", "22mH", "33mH", "47mH", "68mH",
"100mH", "150mH", "220mH", "330mH", "470mH", "680mH"
]
elif component_type == ComponentType.CRYSTAL:
return [
"32.768kHz", "1MHz", "2MHz", "4MHz", "8MHz", "10MHz", "12MHz", "16MHz", "20MHz", "24MHz", "25MHz", "27MHz"
]
else:
return []