- 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>
559 lines
23 KiB
Python
559 lines
23 KiB
Python
"""
|
|
PCB Layer Stack-up Analysis utilities for KiCad.
|
|
|
|
Provides functionality to analyze PCB layer configurations, impedance calculations,
|
|
manufacturing constraints, and design rule validation for multi-layer boards.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from dataclasses import dataclass
|
|
from typing import Dict, List, Optional, Any, Tuple
|
|
import logging
|
|
import math
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class LayerDefinition:
|
|
"""Represents a single layer in the PCB stack-up."""
|
|
name: str
|
|
layer_type: str # "signal", "power", "ground", "dielectric", "soldermask", "silkscreen"
|
|
thickness: float # in mm
|
|
material: str
|
|
dielectric_constant: Optional[float] = None
|
|
loss_tangent: Optional[float] = None
|
|
copper_weight: Optional[float] = None # in oz (for copper layers)
|
|
layer_number: Optional[int] = None
|
|
kicad_layer_id: Optional[str] = None
|
|
|
|
|
|
@dataclass
|
|
class ImpedanceCalculation:
|
|
"""Impedance calculation results for a trace configuration."""
|
|
trace_width: float
|
|
trace_spacing: Optional[float] # For differential pairs
|
|
impedance_single: Optional[float]
|
|
impedance_differential: Optional[float]
|
|
layer_name: str
|
|
reference_layers: List[str]
|
|
calculation_method: str
|
|
|
|
|
|
@dataclass
|
|
class StackupConstraints:
|
|
"""Manufacturing and design constraints for the stack-up."""
|
|
min_trace_width: float
|
|
min_via_drill: float
|
|
min_annular_ring: float
|
|
aspect_ratio_limit: float
|
|
dielectric_thickness_limits: Tuple[float, float]
|
|
copper_weight_options: List[float]
|
|
layer_count_limit: int
|
|
|
|
|
|
@dataclass
|
|
class LayerStackup:
|
|
"""Complete PCB layer stack-up definition."""
|
|
name: str
|
|
layers: List[LayerDefinition]
|
|
total_thickness: float
|
|
layer_count: int
|
|
impedance_calculations: List[ImpedanceCalculation]
|
|
constraints: StackupConstraints
|
|
manufacturing_notes: List[str]
|
|
|
|
|
|
class LayerStackupAnalyzer:
|
|
"""Analyzer for PCB layer stack-up configurations."""
|
|
|
|
def __init__(self):
|
|
"""Initialize the layer stack-up analyzer."""
|
|
self.standard_materials = self._load_standard_materials()
|
|
self.impedance_calculator = ImpedanceCalculator()
|
|
|
|
def _load_standard_materials(self) -> Dict[str, Dict[str, Any]]:
|
|
"""Load standard PCB materials database."""
|
|
return {
|
|
"FR4_Standard": {
|
|
"dielectric_constant": 4.35,
|
|
"loss_tangent": 0.02,
|
|
"description": "Standard FR4 epoxy fiberglass"
|
|
},
|
|
"FR4_High_Tg": {
|
|
"dielectric_constant": 4.2,
|
|
"loss_tangent": 0.015,
|
|
"description": "High Tg FR4 for lead-free soldering"
|
|
},
|
|
"Rogers_4003C": {
|
|
"dielectric_constant": 3.38,
|
|
"loss_tangent": 0.0027,
|
|
"description": "Rogers low-loss hydrocarbon ceramic"
|
|
},
|
|
"Rogers_4350B": {
|
|
"dielectric_constant": 3.48,
|
|
"loss_tangent": 0.0037,
|
|
"description": "Rogers woven glass reinforced hydrocarbon"
|
|
},
|
|
"Polyimide": {
|
|
"dielectric_constant": 3.5,
|
|
"loss_tangent": 0.002,
|
|
"description": "Flexible polyimide substrate"
|
|
},
|
|
"Prepreg_106": {
|
|
"dielectric_constant": 4.2,
|
|
"loss_tangent": 0.02,
|
|
"description": "Standard prepreg 106 glass style"
|
|
},
|
|
"Prepreg_1080": {
|
|
"dielectric_constant": 4.4,
|
|
"loss_tangent": 0.02,
|
|
"description": "Thick prepreg 1080 glass style"
|
|
}
|
|
}
|
|
|
|
def analyze_pcb_stackup(self, pcb_file_path: str) -> LayerStackup:
|
|
"""Analyze PCB file and extract layer stack-up information."""
|
|
try:
|
|
with open(pcb_file_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# Extract layer definitions
|
|
layers = self._parse_layers(content)
|
|
|
|
# Calculate total thickness
|
|
total_thickness = sum(layer.thickness for layer in layers if layer.thickness)
|
|
|
|
# Extract manufacturing constraints
|
|
constraints = self._extract_constraints(content)
|
|
|
|
# Perform impedance calculations
|
|
impedance_calcs = self._calculate_impedances(layers, content)
|
|
|
|
# Generate manufacturing notes
|
|
notes = self._generate_manufacturing_notes(layers, total_thickness)
|
|
|
|
stackup = LayerStackup(
|
|
name=f"PCB_Stackup_{len(layers)}_layers",
|
|
layers=layers,
|
|
total_thickness=total_thickness,
|
|
layer_count=len([l for l in layers if l.layer_type in ["signal", "power", "ground"]]),
|
|
impedance_calculations=impedance_calcs,
|
|
constraints=constraints,
|
|
manufacturing_notes=notes
|
|
)
|
|
|
|
logger.info(f"Analyzed {len(layers)}-layer stack-up with {total_thickness:.3f}mm total thickness")
|
|
return stackup
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to analyze PCB stack-up from {pcb_file_path}: {e}")
|
|
raise
|
|
|
|
def _parse_layers(self, content: str) -> List[LayerDefinition]:
|
|
"""Parse layer definitions from PCB content."""
|
|
layers = []
|
|
|
|
# Extract layer setup section
|
|
setup_match = re.search(r'\(setup[^)]*\(stackup[^)]*\)', content, re.DOTALL)
|
|
if not setup_match:
|
|
# Fallback to basic layer extraction
|
|
return self._parse_basic_layers(content)
|
|
|
|
stackup_content = setup_match.group(0)
|
|
|
|
# Parse individual layers
|
|
layer_pattern = r'\(layer\s+"([^"]+)"\s+\(type\s+(\w+)\)\s*(?:\(thickness\s+([\d.]+)\))?\s*(?:\(material\s+"([^"]+)"\))?'
|
|
|
|
for match in re.finditer(layer_pattern, stackup_content):
|
|
layer_name = match.group(1)
|
|
layer_type = match.group(2)
|
|
thickness = float(match.group(3)) if match.group(3) else None
|
|
material = match.group(4) or "Unknown"
|
|
|
|
# Get material properties
|
|
material_props = self.standard_materials.get(material, {})
|
|
|
|
layer = LayerDefinition(
|
|
name=layer_name,
|
|
layer_type=layer_type,
|
|
thickness=thickness or 0.0,
|
|
material=material,
|
|
dielectric_constant=material_props.get("dielectric_constant"),
|
|
loss_tangent=material_props.get("loss_tangent"),
|
|
copper_weight=1.0 if layer_type in ["signal", "power", "ground"] else None
|
|
)
|
|
layers.append(layer)
|
|
|
|
# If no stack-up found, create standard layers
|
|
if not layers:
|
|
layers = self._create_standard_stackup(content)
|
|
|
|
return layers
|
|
|
|
def _parse_basic_layers(self, content: str) -> List[LayerDefinition]:
|
|
"""Parse basic layer information when detailed stack-up is not available."""
|
|
layers = []
|
|
|
|
# Find layer definitions in PCB
|
|
layer_pattern = r'\((\d+)\s+"([^"]+)"\s+(signal|power|user)\)'
|
|
|
|
found_layers = []
|
|
for match in re.finditer(layer_pattern, content):
|
|
layer_num = int(match.group(1))
|
|
layer_name = match.group(2)
|
|
layer_type = match.group(3)
|
|
found_layers.append((layer_num, layer_name, layer_type))
|
|
|
|
found_layers.sort(key=lambda x: x[0]) # Sort by layer number
|
|
|
|
# Create layer definitions with estimated properties
|
|
for i, (layer_num, layer_name, layer_type) in enumerate(found_layers):
|
|
# Estimate thickness based on layer type and position
|
|
if i == 0 or i == len(found_layers) - 1: # Top/bottom layers
|
|
thickness = 0.035 # 35μm copper
|
|
else:
|
|
thickness = 0.017 # 17μm inner layers
|
|
|
|
layer = LayerDefinition(
|
|
name=layer_name,
|
|
layer_type="signal" if layer_type == "signal" else layer_type,
|
|
thickness=thickness,
|
|
material="Copper",
|
|
copper_weight=1.0,
|
|
layer_number=layer_num,
|
|
kicad_layer_id=str(layer_num)
|
|
)
|
|
layers.append(layer)
|
|
|
|
# Add dielectric layer between copper layers (except after last layer)
|
|
if i < len(found_layers) - 1:
|
|
dielectric_thickness = 0.2 if len(found_layers) <= 4 else 0.1
|
|
dielectric = LayerDefinition(
|
|
name=f"Dielectric_{i+1}",
|
|
layer_type="dielectric",
|
|
thickness=dielectric_thickness,
|
|
material="FR4_Standard",
|
|
dielectric_constant=4.35,
|
|
loss_tangent=0.02
|
|
)
|
|
layers.append(dielectric)
|
|
|
|
return layers
|
|
|
|
def _create_standard_stackup(self, content: str) -> List[LayerDefinition]:
|
|
"""Create a standard 4-layer stack-up when no stack-up is defined."""
|
|
return [
|
|
LayerDefinition("Top", "signal", 0.035, "Copper", copper_weight=1.0),
|
|
LayerDefinition("Prepreg_1", "dielectric", 0.2, "Prepreg_106",
|
|
dielectric_constant=4.2, loss_tangent=0.02),
|
|
LayerDefinition("Inner1", "power", 0.017, "Copper", copper_weight=0.5),
|
|
LayerDefinition("Core", "dielectric", 1.2, "FR4_Standard",
|
|
dielectric_constant=4.35, loss_tangent=0.02),
|
|
LayerDefinition("Inner2", "ground", 0.017, "Copper", copper_weight=0.5),
|
|
LayerDefinition("Prepreg_2", "dielectric", 0.2, "Prepreg_106",
|
|
dielectric_constant=4.2, loss_tangent=0.02),
|
|
LayerDefinition("Bottom", "signal", 0.035, "Copper", copper_weight=1.0)
|
|
]
|
|
|
|
def _extract_constraints(self, content: str) -> StackupConstraints:
|
|
"""Extract manufacturing constraints from PCB."""
|
|
# Default constraints - could be extracted from design rules
|
|
return StackupConstraints(
|
|
min_trace_width=0.1, # 100μm
|
|
min_via_drill=0.2, # 200μm
|
|
min_annular_ring=0.05, # 50μm
|
|
aspect_ratio_limit=8.0, # 8:1 drill depth to diameter
|
|
dielectric_thickness_limits=(0.05, 3.0), # 50μm to 3mm
|
|
copper_weight_options=[0.5, 1.0, 2.0], # oz
|
|
layer_count_limit=16
|
|
)
|
|
|
|
def _calculate_impedances(self, layers: List[LayerDefinition],
|
|
content: str) -> List[ImpedanceCalculation]:
|
|
"""Calculate characteristic impedances for signal layers."""
|
|
impedance_calcs = []
|
|
|
|
signal_layers = [l for l in layers if l.layer_type == "signal"]
|
|
|
|
for signal_layer in signal_layers:
|
|
# Find reference layers (adjacent power/ground planes)
|
|
ref_layers = self._find_reference_layers(signal_layer, layers)
|
|
|
|
# Calculate for standard trace widths
|
|
for trace_width in [0.1, 0.15, 0.2, 0.25]: # mm
|
|
single_ended = self.impedance_calculator.calculate_microstrip_impedance(
|
|
trace_width, signal_layer, layers
|
|
)
|
|
|
|
differential = self.impedance_calculator.calculate_differential_impedance(
|
|
trace_width, 0.15, signal_layer, layers # 0.15mm spacing
|
|
)
|
|
|
|
impedance_calcs.append(ImpedanceCalculation(
|
|
trace_width=trace_width,
|
|
trace_spacing=0.15,
|
|
impedance_single=single_ended,
|
|
impedance_differential=differential,
|
|
layer_name=signal_layer.name,
|
|
reference_layers=ref_layers,
|
|
calculation_method="microstrip"
|
|
))
|
|
|
|
return impedance_calcs
|
|
|
|
def _find_reference_layers(self, signal_layer: LayerDefinition,
|
|
layers: List[LayerDefinition]) -> List[str]:
|
|
"""Find reference planes for a signal layer."""
|
|
ref_layers = []
|
|
signal_idx = layers.index(signal_layer)
|
|
|
|
# Look for adjacent power/ground layers
|
|
for i in range(max(0, signal_idx - 2), min(len(layers), signal_idx + 3)):
|
|
if i != signal_idx and layers[i].layer_type in ["power", "ground"]:
|
|
ref_layers.append(layers[i].name)
|
|
|
|
return ref_layers
|
|
|
|
def _generate_manufacturing_notes(self, layers: List[LayerDefinition],
|
|
total_thickness: float) -> List[str]:
|
|
"""Generate manufacturing and assembly notes."""
|
|
notes = []
|
|
|
|
copper_layers = len([l for l in layers if l.layer_type in ["signal", "power", "ground"]])
|
|
|
|
if copper_layers > 8:
|
|
notes.append("High layer count may require specialized manufacturing")
|
|
|
|
if total_thickness > 3.0:
|
|
notes.append("Thick board may require extended drill programs")
|
|
elif total_thickness < 0.8:
|
|
notes.append("Thin board requires careful handling during assembly")
|
|
|
|
# Check for impedance control requirements
|
|
signal_layers = len([l for l in layers if l.layer_type == "signal"])
|
|
if signal_layers > 2:
|
|
notes.append("Multi-layer design - impedance control recommended")
|
|
|
|
# Material considerations
|
|
materials = set(l.material for l in layers if l.layer_type == "dielectric")
|
|
if len(materials) > 1:
|
|
notes.append("Mixed dielectric materials - verify thermal expansion compatibility")
|
|
|
|
return notes
|
|
|
|
def validate_stackup(self, stackup: LayerStackup) -> List[str]:
|
|
"""Validate stack-up for manufacturability and design rules."""
|
|
issues = []
|
|
|
|
# Check layer count
|
|
if stackup.layer_count > stackup.constraints.layer_count_limit:
|
|
issues.append(f"Layer count {stackup.layer_count} exceeds limit of {stackup.constraints.layer_count_limit}")
|
|
|
|
# Check total thickness
|
|
if stackup.total_thickness > 6.0:
|
|
issues.append(f"Total thickness {stackup.total_thickness:.2f}mm may be difficult to manufacture")
|
|
|
|
# Check for proper reference planes
|
|
signal_layers = [l for l in stackup.layers if l.layer_type == "signal"]
|
|
power_ground_layers = [l for l in stackup.layers if l.layer_type in ["power", "ground"]]
|
|
|
|
if len(signal_layers) > 2 and len(power_ground_layers) < 2:
|
|
issues.append("Multi-layer design should have dedicated power and ground planes")
|
|
|
|
# Check dielectric thickness
|
|
for layer in stackup.layers:
|
|
if layer.layer_type == "dielectric":
|
|
if layer.thickness < stackup.constraints.dielectric_thickness_limits[0]:
|
|
issues.append(f"Dielectric layer '{layer.name}' thickness {layer.thickness:.3f}mm is too thin")
|
|
elif layer.thickness > stackup.constraints.dielectric_thickness_limits[1]:
|
|
issues.append(f"Dielectric layer '{layer.name}' thickness {layer.thickness:.3f}mm is too thick")
|
|
|
|
# Check copper balance
|
|
top_copper = sum(l.thickness for l in stackup.layers[:len(stackup.layers)//2] if l.copper_weight)
|
|
bottom_copper = sum(l.thickness for l in stackup.layers[len(stackup.layers)//2:] if l.copper_weight)
|
|
|
|
if abs(top_copper - bottom_copper) / max(top_copper, bottom_copper) > 0.3:
|
|
issues.append("Copper distribution is unbalanced - may cause warpage")
|
|
|
|
return issues
|
|
|
|
def generate_stackup_report(self, stackup: LayerStackup) -> Dict[str, Any]:
|
|
"""Generate comprehensive stack-up analysis report."""
|
|
validation_issues = self.validate_stackup(stackup)
|
|
|
|
# Calculate electrical properties
|
|
electrical_props = self._calculate_electrical_properties(stackup)
|
|
|
|
# Generate recommendations
|
|
recommendations = self._generate_stackup_recommendations(stackup, validation_issues)
|
|
|
|
return {
|
|
"stackup_info": {
|
|
"name": stackup.name,
|
|
"layer_count": stackup.layer_count,
|
|
"total_thickness_mm": stackup.total_thickness,
|
|
"copper_layers": len([l for l in stackup.layers if l.copper_weight]),
|
|
"dielectric_layers": len([l for l in stackup.layers if l.layer_type == "dielectric"])
|
|
},
|
|
"layer_details": [
|
|
{
|
|
"name": layer.name,
|
|
"type": layer.layer_type,
|
|
"thickness_mm": layer.thickness,
|
|
"material": layer.material,
|
|
"dielectric_constant": layer.dielectric_constant,
|
|
"loss_tangent": layer.loss_tangent,
|
|
"copper_weight_oz": layer.copper_weight
|
|
}
|
|
for layer in stackup.layers
|
|
],
|
|
"impedance_analysis": [
|
|
{
|
|
"layer": imp.layer_name,
|
|
"trace_width_mm": imp.trace_width,
|
|
"single_ended_ohm": imp.impedance_single,
|
|
"differential_ohm": imp.impedance_differential,
|
|
"reference_layers": imp.reference_layers
|
|
}
|
|
for imp in stackup.impedance_calculations
|
|
],
|
|
"electrical_properties": electrical_props,
|
|
"manufacturing": {
|
|
"constraints": {
|
|
"min_trace_width_mm": stackup.constraints.min_trace_width,
|
|
"min_via_drill_mm": stackup.constraints.min_via_drill,
|
|
"aspect_ratio_limit": stackup.constraints.aspect_ratio_limit
|
|
},
|
|
"notes": stackup.manufacturing_notes
|
|
},
|
|
"validation": {
|
|
"issues": validation_issues,
|
|
"passed": len(validation_issues) == 0
|
|
},
|
|
"recommendations": recommendations
|
|
}
|
|
|
|
def _calculate_electrical_properties(self, stackup: LayerStackup) -> Dict[str, Any]:
|
|
"""Calculate overall electrical properties of the stack-up."""
|
|
# Calculate effective dielectric constant
|
|
dielectric_layers = [l for l in stackup.layers if l.layer_type == "dielectric" and l.dielectric_constant]
|
|
|
|
if dielectric_layers:
|
|
weighted_dk = sum(l.dielectric_constant * l.thickness for l in dielectric_layers) / sum(l.thickness for l in dielectric_layers)
|
|
avg_loss_tangent = sum(l.loss_tangent or 0 for l in dielectric_layers) / len(dielectric_layers)
|
|
else:
|
|
weighted_dk = 4.35 # Default FR4
|
|
avg_loss_tangent = 0.02
|
|
|
|
return {
|
|
"effective_dielectric_constant": weighted_dk,
|
|
"average_loss_tangent": avg_loss_tangent,
|
|
"total_copper_thickness_mm": sum(l.thickness for l in stackup.layers if l.copper_weight),
|
|
"total_dielectric_thickness_mm": sum(l.thickness for l in stackup.layers if l.layer_type == "dielectric")
|
|
}
|
|
|
|
def _generate_stackup_recommendations(self, stackup: LayerStackup,
|
|
issues: List[str]) -> List[str]:
|
|
"""Generate recommendations for stack-up optimization."""
|
|
recommendations = []
|
|
|
|
if issues:
|
|
recommendations.append("Address validation issues before manufacturing")
|
|
|
|
# Impedance recommendations
|
|
impedance_50ohm = [imp for imp in stackup.impedance_calculations if imp.impedance_single and abs(imp.impedance_single - 50) < 5]
|
|
if not impedance_50ohm and stackup.impedance_calculations:
|
|
recommendations.append("Consider adjusting trace widths to achieve 50Ω characteristic impedance")
|
|
|
|
# Layer count recommendations
|
|
if stackup.layer_count == 2:
|
|
recommendations.append("Consider 4-layer stack-up for better signal integrity and power distribution")
|
|
elif stackup.layer_count > 8:
|
|
recommendations.append("High layer count - ensure proper via management and signal routing")
|
|
|
|
# Material recommendations
|
|
materials = set(l.material for l in stackup.layers if l.layer_type == "dielectric")
|
|
if "Rogers" in str(materials) and "FR4" in str(materials):
|
|
recommendations.append("Mixed materials detected - verify thermal expansion compatibility")
|
|
|
|
return recommendations
|
|
|
|
|
|
class ImpedanceCalculator:
|
|
"""Calculator for transmission line impedance."""
|
|
|
|
def calculate_microstrip_impedance(self, trace_width: float, signal_layer: LayerDefinition,
|
|
layers: List[LayerDefinition]) -> Optional[float]:
|
|
"""Calculate microstrip impedance for a trace."""
|
|
try:
|
|
# Find the dielectric layer below the signal layer
|
|
signal_idx = layers.index(signal_layer)
|
|
dielectric = None
|
|
|
|
for i in range(signal_idx + 1, len(layers)):
|
|
if layers[i].layer_type == "dielectric":
|
|
dielectric = layers[i]
|
|
break
|
|
|
|
if not dielectric or not dielectric.dielectric_constant:
|
|
return None
|
|
|
|
# Microstrip impedance calculation (simplified)
|
|
h = dielectric.thickness # dielectric height
|
|
w = trace_width # trace width
|
|
er = dielectric.dielectric_constant
|
|
|
|
# Wheeler's formula for microstrip impedance
|
|
if w/h > 1:
|
|
z0 = (120 * math.pi) / (math.sqrt(er) * (w/h + 1.393 + 0.667 * math.log(w/h + 1.444)))
|
|
else:
|
|
z0 = (60 * math.log(8*h/w + w/(4*h))) / math.sqrt(er)
|
|
|
|
return round(z0, 1)
|
|
|
|
except (ValueError, ZeroDivisionError, IndexError):
|
|
return None
|
|
|
|
def calculate_differential_impedance(self, trace_width: float, trace_spacing: float,
|
|
signal_layer: LayerDefinition,
|
|
layers: List[LayerDefinition]) -> Optional[float]:
|
|
"""Calculate differential impedance for a trace pair."""
|
|
try:
|
|
single_ended = self.calculate_microstrip_impedance(trace_width, signal_layer, layers)
|
|
if not single_ended:
|
|
return None
|
|
|
|
# Find the dielectric layer below the signal layer
|
|
signal_idx = layers.index(signal_layer)
|
|
dielectric = None
|
|
|
|
for i in range(signal_idx + 1, len(layers)):
|
|
if layers[i].layer_type == "dielectric":
|
|
dielectric = layers[i]
|
|
break
|
|
|
|
if not dielectric:
|
|
return None
|
|
|
|
# Approximate differential impedance calculation
|
|
h = dielectric.thickness
|
|
w = trace_width
|
|
s = trace_spacing
|
|
|
|
# Coupling factor (simplified)
|
|
k = s / (s + 2*w)
|
|
|
|
# Differential impedance approximation
|
|
z_diff = 2 * single_ended * (1 - k)
|
|
|
|
return round(z_diff, 1)
|
|
|
|
except (ValueError, ZeroDivisionError):
|
|
return None
|
|
|
|
|
|
def create_stackup_analyzer() -> LayerStackupAnalyzer:
|
|
"""Create and initialize a layer stack-up analyzer."""
|
|
return LayerStackupAnalyzer() |