""" Layer Stack-up Analysis Tools for KiCad MCP Server. Provides MCP tools for analyzing PCB layer configurations, impedance calculations, and manufacturing constraints for multi-layer board designs. """ import json from typing import Any, Dict, List from fastmcp import FastMCP from kicad_mcp.utils.layer_stackup import ( create_stackup_analyzer, LayerStackupAnalyzer ) from kicad_mcp.utils.path_validator import validate_kicad_file def register_layer_tools(mcp: FastMCP) -> None: """Register layer stack-up analysis tools with the MCP server.""" @mcp.tool() def analyze_pcb_stackup(pcb_file_path: str) -> Dict[str, Any]: """ Analyze PCB layer stack-up configuration and properties. Extracts layer definitions, calculates impedances, validates manufacturing constraints, and provides recommendations for multi-layer board design. Args: pcb_file_path: Path to the .kicad_pcb file to analyze Returns: Dictionary containing comprehensive stack-up analysis """ try: # Validate PCB file validated_path = validate_kicad_file(pcb_file_path, "pcb") # Create analyzer and perform analysis analyzer = create_stackup_analyzer() stackup = analyzer.analyze_pcb_stackup(validated_path) # Generate comprehensive report report = analyzer.generate_stackup_report(stackup) return { "success": True, "pcb_file": validated_path, "stackup_analysis": report } except Exception as e: return { "success": False, "error": str(e), "pcb_file": pcb_file_path } @mcp.tool() def calculate_trace_impedance(pcb_file_path: str, trace_width: float, layer_name: str = None, spacing: float = None) -> Dict[str, Any]: """ Calculate characteristic impedance for specific trace configurations. Computes single-ended and differential impedance values based on stack-up configuration and trace geometry parameters. Args: pcb_file_path: Full path to the .kicad_pcb file to analyze trace_width: Trace width in millimeters (e.g., 0.15 for 150μm traces) layer_name: Specific layer name to calculate for (optional - if omitted, calculates for all signal layers) spacing: Trace spacing for differential pairs in mm (e.g., 0.15 for 150μm spacing) Returns: Dictionary with impedance values, recommendations for 50Ω/100Ω targets Examples: calculate_trace_impedance("/path/to/board.kicad_pcb", 0.15) calculate_trace_impedance("/path/to/board.kicad_pcb", 0.1, "Top", 0.15) """ try: validated_path = validate_kicad_file(pcb_file_path, "pcb") analyzer = create_stackup_analyzer() stackup = analyzer.analyze_pcb_stackup(validated_path) # Filter signal layers signal_layers = [l for l in stackup.layers if l.layer_type == "signal"] if layer_name: signal_layers = [l for l in signal_layers if l.name == layer_name] if not signal_layers: return { "success": False, "error": f"Layer '{layer_name}' not found or not a signal layer" } impedance_results = [] for layer in signal_layers: # Calculate single-ended impedance single_ended = analyzer.impedance_calculator.calculate_microstrip_impedance( trace_width, layer, stackup.layers ) # Calculate differential impedance if spacing provided differential = None if spacing is not None: differential = analyzer.impedance_calculator.calculate_differential_impedance( trace_width, spacing, layer, stackup.layers ) # Find reference layers ref_layers = analyzer._find_reference_layers(layer, stackup.layers) impedance_results.append({ "layer_name": layer.name, "trace_width_mm": trace_width, "spacing_mm": spacing, "single_ended_impedance_ohm": single_ended, "differential_impedance_ohm": differential, "reference_layers": ref_layers, "dielectric_thickness_mm": _get_dielectric_thickness(layer, stackup.layers), "dielectric_constant": _get_dielectric_constant(layer, stackup.layers) }) # Generate recommendations recommendations = [] for result in impedance_results: if result["single_ended_impedance_ohm"]: impedance = result["single_ended_impedance_ohm"] if abs(impedance - 50) > 10: if impedance > 50: recommendations.append(f"Increase trace width on {result['layer_name']} to reduce impedance") else: recommendations.append(f"Decrease trace width on {result['layer_name']} to increase impedance") return { "success": True, "pcb_file": validated_path, "impedance_calculations": impedance_results, "target_impedances": { "single_ended": "50Ω typical", "differential": "90Ω or 100Ω typical" }, "recommendations": recommendations } except Exception as e: return { "success": False, "error": str(e), "pcb_file": pcb_file_path } def _get_dielectric_thickness(self, signal_layer, layers): """Get thickness of dielectric layer below signal layer.""" try: signal_idx = layers.index(signal_layer) for i in range(signal_idx + 1, len(layers)): if layers[i].layer_type == "dielectric": return layers[i].thickness return None except (ValueError, IndexError): return None def _get_dielectric_constant(self, signal_layer, layers): """Get dielectric constant of layer below signal layer.""" try: signal_idx = layers.index(signal_layer) for i in range(signal_idx + 1, len(layers)): if layers[i].layer_type == "dielectric": return layers[i].dielectric_constant return None except (ValueError, IndexError): return None @mcp.tool() def validate_stackup_manufacturing(pcb_file_path: str) -> Dict[str, Any]: """ Validate PCB stack-up against manufacturing constraints. Checks layer configuration, thicknesses, materials, and design rules for manufacturability and identifies potential production issues. Args: pcb_file_path: Path to the .kicad_pcb file Returns: Dictionary containing validation results and manufacturing recommendations """ try: validated_path = validate_kicad_file(pcb_file_path, "pcb") analyzer = create_stackup_analyzer() stackup = analyzer.analyze_pcb_stackup(validated_path) # Validate stack-up validation_issues = analyzer.validate_stackup(stackup) # Check additional manufacturing constraints manufacturing_checks = self._perform_manufacturing_checks(stackup) # Combine all issues all_issues = validation_issues + manufacturing_checks["issues"] return { "success": True, "pcb_file": validated_path, "validation_results": { "passed": len(all_issues) == 0, "total_issues": len(all_issues), "issues": all_issues, "severity_breakdown": { "critical": len([i for i in all_issues if "exceeds limit" in i or "too thin" in i]), "warnings": len([i for i in all_issues if "should" in i or "may" in i]) } }, "stackup_summary": { "layer_count": stackup.layer_count, "total_thickness_mm": stackup.total_thickness, "copper_layers": len([l for l in stackup.layers if l.copper_weight]), "signal_layers": len([l for l in stackup.layers if l.layer_type == "signal"]) }, "manufacturing_assessment": manufacturing_checks["assessment"], "cost_implications": self._assess_cost_implications(stackup), "recommendations": stackup.manufacturing_notes + manufacturing_checks["recommendations"] } except Exception as e: return { "success": False, "error": str(e), "pcb_file": pcb_file_path } def _perform_manufacturing_checks(self, stackup): """Perform additional manufacturing feasibility checks.""" issues = [] recommendations = [] # Check aspect ratio for drilling copper_thickness = sum(l.thickness for l in stackup.layers if l.copper_weight) max_drill_depth = stackup.total_thickness min_drill_diameter = stackup.constraints.min_via_drill aspect_ratio = max_drill_depth / min_drill_diameter if aspect_ratio > stackup.constraints.aspect_ratio_limit: issues.append(f"Aspect ratio {aspect_ratio:.1f}:1 exceeds manufacturing limit") recommendations.append("Consider using buried/blind vias or increasing minimum drill size") # Check copper balance top_half_copper = sum(l.thickness for l in stackup.layers[:len(stackup.layers)//2] if l.copper_weight) bottom_half_copper = sum(l.thickness for l in stackup.layers[len(stackup.layers)//2:] if l.copper_weight) if abs(top_half_copper - bottom_half_copper) / max(top_half_copper, bottom_half_copper) > 0.4: issues.append("Copper distribution imbalance may cause board warpage") recommendations.append("Redistribute copper or add balancing copper fills") # Assess manufacturing complexity complexity_factors = [] if stackup.layer_count > 6: complexity_factors.append("High layer count") if stackup.total_thickness > 2.5: complexity_factors.append("Thick board") if len(set(l.material for l in stackup.layers if l.layer_type == "dielectric")) > 1: complexity_factors.append("Mixed dielectric materials") assessment = "Standard" if not complexity_factors else f"Complex ({', '.join(complexity_factors)})" return { "issues": issues, "recommendations": recommendations, "assessment": assessment } def _assess_cost_implications(self, stackup): """Assess cost implications of the stack-up design.""" cost_factors = [] cost_multiplier = 1.0 # Layer count impact if stackup.layer_count > 4: cost_multiplier *= (1.0 + (stackup.layer_count - 4) * 0.15) cost_factors.append(f"{stackup.layer_count}-layer design increases cost") # Thickness impact if stackup.total_thickness > 1.6: cost_multiplier *= 1.1 cost_factors.append("Non-standard thickness increases cost") # Material impact premium_materials = ["Rogers", "Polyimide"] if any(material in str(stackup.layers) for material in premium_materials): cost_multiplier *= 1.3 cost_factors.append("Premium materials increase cost significantly") cost_category = "Low" if cost_multiplier < 1.2 else "Medium" if cost_multiplier < 1.5 else "High" return { "cost_category": cost_category, "cost_multiplier": round(cost_multiplier, 2), "cost_factors": cost_factors, "optimization_suggestions": [ "Consider standard 4-layer stack-up for cost reduction", "Use standard FR4 materials where possible", "Optimize thickness to standard values (1.6mm typical)" ] if cost_multiplier > 1.3 else ["Current design is cost-optimized"] } @mcp.tool() def optimize_stackup_for_impedance(pcb_file_path: str, target_impedance: float = 50.0, differential_target: float = 100.0) -> Dict[str, Any]: """ Optimize stack-up configuration for target impedance values. Suggests modifications to layer thicknesses and trace widths to achieve desired characteristic impedance for signal integrity. Args: pcb_file_path: Path to the .kicad_pcb file target_impedance: Target single-ended impedance in ohms (default: 50Ω) differential_target: Target differential impedance in ohms (default: 100Ω) Returns: Dictionary containing optimization recommendations and calculations """ try: validated_path = validate_kicad_file(pcb_file_path, "pcb") analyzer = create_stackup_analyzer() stackup = analyzer.analyze_pcb_stackup(validated_path) optimization_results = [] # Analyze each signal layer signal_layers = [l for l in stackup.layers if l.layer_type == "signal"] for layer in signal_layers: layer_optimization = self._optimize_layer_impedance( layer, stackup.layers, analyzer, target_impedance, differential_target ) optimization_results.append(layer_optimization) # Generate overall recommendations overall_recommendations = self._generate_impedance_recommendations( optimization_results, target_impedance, differential_target ) return { "success": True, "pcb_file": validated_path, "target_impedances": { "single_ended": target_impedance, "differential": differential_target }, "layer_optimizations": optimization_results, "overall_recommendations": overall_recommendations, "implementation_notes": [ "Impedance optimization may require stack-up modifications", "Verify with manufacturer before finalizing changes", "Consider tolerance requirements for critical nets", "Update design rules after stack-up modifications" ] } except Exception as e: return { "success": False, "error": str(e), "pcb_file": pcb_file_path } def _optimize_layer_impedance(self, layer, layers, analyzer, target_se, target_diff): """Optimize impedance for a specific layer.""" current_impedances = [] optimized_suggestions = [] # Test different trace widths test_widths = [0.08, 0.1, 0.125, 0.15, 0.2, 0.25, 0.3] for width in test_widths: se_impedance = analyzer.impedance_calculator.calculate_microstrip_impedance( width, layer, layers ) diff_impedance = analyzer.impedance_calculator.calculate_differential_impedance( width, 0.15, layer, layers # 0.15mm spacing ) if se_impedance: current_impedances.append({ "trace_width_mm": width, "single_ended_ohm": se_impedance, "differential_ohm": diff_impedance, "se_error": abs(se_impedance - target_se), "diff_error": abs(diff_impedance - target_diff) if diff_impedance else None }) # Find best matches best_se = min(current_impedances, key=lambda x: x["se_error"]) if current_impedances else None best_diff = min([x for x in current_impedances if x["diff_error"] is not None], key=lambda x: x["diff_error"]) if any(x["diff_error"] is not None for x in current_impedances) else None return { "layer_name": layer.name, "current_impedances": current_impedances, "recommended_for_single_ended": best_se, "recommended_for_differential": best_diff, "optimization_notes": self._generate_layer_optimization_notes( layer, best_se, best_diff, target_se, target_diff ) } def _generate_layer_optimization_notes(self, layer, best_se, best_diff, target_se, target_diff): """Generate optimization notes for a specific layer.""" notes = [] if best_se and abs(best_se["se_error"]) > 5: notes.append(f"Difficult to achieve {target_se}Ω on {layer.name} with current stack-up") notes.append("Consider adjusting dielectric thickness or material") if best_diff and best_diff["diff_error"] and abs(best_diff["diff_error"]) > 10: notes.append(f"Difficult to achieve {target_diff}Ω differential on {layer.name}") notes.append("Consider adjusting trace spacing or dielectric properties") return notes def _generate_impedance_recommendations(self, optimization_results, target_se, target_diff): """Generate overall impedance optimization recommendations.""" recommendations = [] # Check if any layers have poor impedance control poor_control_layers = [] for result in optimization_results: if result["recommended_for_single_ended"] and result["recommended_for_single_ended"]["se_error"] > 5: poor_control_layers.append(result["layer_name"]) if poor_control_layers: recommendations.append(f"Layers with poor impedance control: {', '.join(poor_control_layers)}") recommendations.append("Consider stack-up redesign or use impedance-optimized prepregs") # Check for consistent trace widths trace_widths = set() for result in optimization_results: if result["recommended_for_single_ended"]: trace_widths.add(result["recommended_for_single_ended"]["trace_width_mm"]) if len(trace_widths) > 2: recommendations.append("Multiple trace widths needed - consider design rule complexity") return recommendations @mcp.tool() def compare_stackup_alternatives(pcb_file_path: str, alternative_configs: List[Dict[str, Any]] = None) -> Dict[str, Any]: """ Compare different stack-up alternatives for the same design. Evaluates multiple stack-up configurations against cost, performance, and manufacturing criteria to help select optimal configuration. Args: pcb_file_path: Path to the .kicad_pcb file alternative_configs: List of alternative stack-up configurations (optional) Returns: Dictionary containing comparison results and recommendations """ try: validated_path = validate_kicad_file(pcb_file_path, "pcb") analyzer = create_stackup_analyzer() current_stackup = analyzer.analyze_pcb_stackup(validated_path) # Generate standard alternatives if none provided if not alternative_configs: alternative_configs = self._generate_standard_alternatives(current_stackup) comparison_results = [] # Analyze current stackup current_analysis = { "name": "Current Design", "stackup": current_stackup, "report": analyzer.generate_stackup_report(current_stackup), "score": self._calculate_stackup_score(current_stackup, analyzer) } comparison_results.append(current_analysis) # Analyze alternatives for i, config in enumerate(alternative_configs): alt_stackup = self._create_alternative_stackup(current_stackup, config) alt_report = analyzer.generate_stackup_report(alt_stackup) alt_score = self._calculate_stackup_score(alt_stackup, analyzer) comparison_results.append({ "name": config.get("name", f"Alternative {i+1}"), "stackup": alt_stackup, "report": alt_report, "score": alt_score }) # Rank alternatives ranked_results = sorted(comparison_results, key=lambda x: x["score"]["total"], reverse=True) return { "success": True, "pcb_file": validated_path, "comparison_results": [ { "name": result["name"], "layer_count": result["stackup"].layer_count, "total_thickness_mm": result["stackup"].total_thickness, "total_score": result["score"]["total"], "cost_score": result["score"]["cost"], "performance_score": result["score"]["performance"], "manufacturing_score": result["score"]["manufacturing"], "validation_passed": result["report"]["validation"]["passed"], "key_advantages": self._identify_advantages(result, comparison_results), "key_disadvantages": self._identify_disadvantages(result, comparison_results) } for result in ranked_results ], "recommendation": { "best_overall": ranked_results[0]["name"], "best_cost": min(comparison_results, key=lambda x: x["score"]["cost"])["name"], "best_performance": max(comparison_results, key=lambda x: x["score"]["performance"])["name"], "reasoning": self._generate_recommendation_reasoning(ranked_results) } } except Exception as e: return { "success": False, "error": str(e), "pcb_file": pcb_file_path } def _generate_standard_alternatives(self, current_stackup): """Generate standard alternative stack-up configurations.""" alternatives = [] current_layers = current_stackup.layer_count # 4-layer alternative (if current is different) if current_layers != 4: alternatives.append({ "name": "4-Layer Standard", "layer_count": 4, "description": "Standard 4-layer stack-up for cost optimization" }) # 6-layer alternative (if current is different and > 4) if current_layers > 4 and current_layers != 6: alternatives.append({ "name": "6-Layer Balanced", "layer_count": 6, "description": "6-layer stack-up for improved power distribution" }) # High-performance alternative if current_layers <= 8: alternatives.append({ "name": "High-Performance", "layer_count": min(current_layers + 2, 10), "description": "Additional layers for better signal integrity" }) return alternatives def _create_alternative_stackup(self, base_stackup, config): """Create an alternative stack-up based on configuration.""" # This is a simplified implementation - in practice, you'd need # more sophisticated stack-up generation based on the configuration alt_stackup = base_stackup # For now, return the same stack-up # TODO: Implement actual alternative stack-up generation return alt_stackup def _calculate_stackup_score(self, stackup, analyzer): """Calculate overall score for stack-up quality.""" # Cost score (lower is better, invert for scoring) cost_score = 100 - min(stackup.layer_count * 5, 50) # Penalize high layer count # Performance score performance_score = 70 # Base score if stackup.layer_count >= 4: performance_score += 20 # Dedicated power planes if stackup.total_thickness < 2.0: performance_score += 10 # Good for high-frequency # Manufacturing score validation_issues = analyzer.validate_stackup(stackup) manufacturing_score = 100 - len(validation_issues) * 10 total_score = (cost_score * 0.3 + performance_score * 0.4 + manufacturing_score * 0.3) return { "total": round(total_score, 1), "cost": cost_score, "performance": performance_score, "manufacturing": manufacturing_score } def _identify_advantages(self, result, all_results): """Identify key advantages of a stack-up configuration.""" advantages = [] if result["score"]["cost"] == max(r["score"]["cost"] for r in all_results): advantages.append("Lowest cost option") if result["score"]["performance"] == max(r["score"]["performance"] for r in all_results): advantages.append("Best performance characteristics") if result["report"]["validation"]["passed"]: advantages.append("Passes all manufacturing validation") return advantages[:3] # Limit to top 3 advantages def _identify_disadvantages(self, result, all_results): """Identify key disadvantages of a stack-up configuration.""" disadvantages = [] if result["score"]["cost"] == min(r["score"]["cost"] for r in all_results): disadvantages.append("Highest cost option") if not result["report"]["validation"]["passed"]: disadvantages.append("Has manufacturing validation issues") if result["stackup"].layer_count > 8: disadvantages.append("Complex manufacturing due to high layer count") return disadvantages[:3] # Limit to top 3 disadvantages def _generate_recommendation_reasoning(self, ranked_results): """Generate reasoning for the recommendation.""" best = ranked_results[0] reasoning = f"'{best['name']}' is recommended due to its high overall score ({best['score']['total']:.1f}/100). " if best["report"]["validation"]["passed"]: reasoning += "It passes all manufacturing validation checks and " if best["score"]["cost"] > 70: reasoning += "offers good cost efficiency." elif best["score"]["performance"] > 80: reasoning += "provides excellent performance characteristics." else: reasoning += "offers the best balance of cost, performance, and manufacturability." return reasoning