- 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>
650 lines
28 KiB
Python
650 lines
28 KiB
Python
"""
|
|
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 |