""" Boundary validation system for KiCad circuit generation. Provides comprehensive validation for component positioning, boundary checking, and validation report generation to prevent out-of-bounds placement issues. """ from dataclasses import dataclass from enum import Enum import json from typing import Any from kicad_mcp.utils.component_layout import ComponentLayoutManager, SchematicBounds from kicad_mcp.utils.coordinate_converter import CoordinateConverter, validate_position class ValidationSeverity(Enum): """Severity levels for validation issues.""" ERROR = "error" WARNING = "warning" INFO = "info" @dataclass class ValidationIssue: """Represents a validation issue found during boundary checking.""" severity: ValidationSeverity component_ref: str message: str position: tuple[float, float] suggested_position: tuple[float, float] | None = None component_type: str = "default" @dataclass class ValidationReport: """Comprehensive validation report for circuit positioning.""" success: bool issues: list[ValidationIssue] total_components: int validated_components: int out_of_bounds_count: int corrected_positions: dict[str, tuple[float, float]] def has_errors(self) -> bool: """Check if report contains any error-level issues.""" return any(issue.severity == ValidationSeverity.ERROR for issue in self.issues) def has_warnings(self) -> bool: """Check if report contains any warning-level issues.""" return any(issue.severity == ValidationSeverity.WARNING for issue in self.issues) def get_issues_by_severity(self, severity: ValidationSeverity) -> list[ValidationIssue]: """Get all issues of a specific severity level.""" return [issue for issue in self.issues if issue.severity == severity] class BoundaryValidator: """ Comprehensive boundary validation system for KiCad circuit generation. Features: - Pre-generation coordinate validation - Automatic position correction - Detailed validation reports - Integration with circuit generation pipeline """ def __init__(self, bounds: SchematicBounds | None = None): """ Initialize the boundary validator. Args: bounds: Schematic boundaries (defaults to A4) """ self.bounds = bounds or SchematicBounds() self.converter = CoordinateConverter() self.layout_manager = ComponentLayoutManager(self.bounds) def validate_component_position( self, component_ref: str, x: float, y: float, component_type: str = "default" ) -> ValidationIssue: """ Validate a single component position. Args: component_ref: Component reference (e.g., "R1") x: X coordinate in mm y: Y coordinate in mm component_type: Type of component Returns: ValidationIssue describing the validation result """ # Check if position is within A4 bounds if not validate_position(x, y, use_margins=True): # Find a corrected position corrected_x, corrected_y = self.layout_manager.find_valid_position( component_ref, component_type, x, y ) return ValidationIssue( severity=ValidationSeverity.ERROR, component_ref=component_ref, message=f"Component {component_ref} at ({x:.2f}, {y:.2f}) is outside A4 bounds", position=(x, y), suggested_position=(corrected_x, corrected_y), component_type=component_type, ) # Check if position is within usable area (with margins) if not validate_position(x, y, use_margins=False): # Position is within absolute bounds but outside usable area return ValidationIssue( severity=ValidationSeverity.WARNING, component_ref=component_ref, message=f"Component {component_ref} at ({x:.2f}, {y:.2f}) is outside usable area (margins)", position=(x, y), component_type=component_type, ) # Position is valid return ValidationIssue( severity=ValidationSeverity.INFO, component_ref=component_ref, message=f"Component {component_ref} position is valid", position=(x, y), component_type=component_type, ) def validate_circuit_components(self, components: list[dict[str, Any]]) -> ValidationReport: """ Validate positioning for all components in a circuit. Args: components: List of component dictionaries with position information Returns: ValidationReport with comprehensive validation results """ issues = [] corrected_positions = {} out_of_bounds_count = 0 # Reset layout manager for this validation self.layout_manager.clear_layout() for component in components: component_ref = component.get("reference", "Unknown") component_type = component.get("component_type", "default") # Extract position - handle different formats position = component.get("position") if position is None: # No position specified - this is an info issue issues.append( ValidationIssue( severity=ValidationSeverity.INFO, component_ref=component_ref, message=f"Component {component_ref} has no position specified", position=(0, 0), component_type=component_type, ) ) continue # Handle position as tuple or list if isinstance(position, list | tuple) and len(position) >= 2: x, y = float(position[0]), float(position[1]) else: issues.append( ValidationIssue( severity=ValidationSeverity.ERROR, component_ref=component_ref, message=f"Component {component_ref} has invalid position format: {position}", position=(0, 0), component_type=component_type, ) ) continue # Validate the position validation_issue = self.validate_component_position(component_ref, x, y, component_type) issues.append(validation_issue) # Track out of bounds components if validation_issue.severity == ValidationSeverity.ERROR: out_of_bounds_count += 1 if validation_issue.suggested_position: corrected_positions[component_ref] = validation_issue.suggested_position # Generate report report = ValidationReport( success=out_of_bounds_count == 0, issues=issues, total_components=len(components), validated_components=len([c for c in components if c.get("position") is not None]), out_of_bounds_count=out_of_bounds_count, corrected_positions=corrected_positions, ) return report def validate_wire_connection( self, start_x: float, start_y: float, end_x: float, end_y: float ) -> list[ValidationIssue]: """ Validate wire connection endpoints. Args: start_x: Starting X coordinate in mm start_y: Starting Y coordinate in mm end_x: Ending X coordinate in mm end_y: Ending Y coordinate in mm Returns: List of validation issues for wire endpoints """ issues = [] # Validate start point if not validate_position(start_x, start_y, use_margins=True): issues.append( ValidationIssue( severity=ValidationSeverity.ERROR, component_ref="WIRE_START", message=f"Wire start point ({start_x:.2f}, {start_y:.2f}) is outside bounds", position=(start_x, start_y), ) ) # Validate end point if not validate_position(end_x, end_y, use_margins=True): issues.append( ValidationIssue( severity=ValidationSeverity.ERROR, component_ref="WIRE_END", message=f"Wire end point ({end_x:.2f}, {end_y:.2f}) is outside bounds", position=(end_x, end_y), ) ) return issues def auto_correct_positions( self, components: list[dict[str, Any]] ) -> tuple[list[dict[str, Any]], ValidationReport]: """ Automatically correct out-of-bounds component positions. Args: components: List of component dictionaries Returns: Tuple of (corrected_components, validation_report) """ # First validate to get correction suggestions validation_report = self.validate_circuit_components(components) # Apply corrections corrected_components = [] for component in components: component_ref = component.get("reference", "Unknown") if component_ref in validation_report.corrected_positions: # Apply correction corrected_component = component.copy() corrected_component["position"] = validation_report.corrected_positions[ component_ref ] corrected_components.append(corrected_component) else: corrected_components.append(component) return corrected_components, validation_report def generate_validation_report_text(self, report: ValidationReport) -> str: """ Generate a human-readable validation report. Args: report: ValidationReport to format Returns: Formatted text report """ lines = [] lines.append("=" * 60) lines.append("BOUNDARY VALIDATION REPORT") lines.append("=" * 60) # Summary lines.append(f"Status: {'PASS' if report.success else 'FAIL'}") lines.append(f"Total Components: {report.total_components}") lines.append(f"Validated Components: {report.validated_components}") lines.append(f"Out of Bounds: {report.out_of_bounds_count}") lines.append(f"Corrected Positions: {len(report.corrected_positions)}") lines.append("") # Issues by severity errors = report.get_issues_by_severity(ValidationSeverity.ERROR) warnings = report.get_issues_by_severity(ValidationSeverity.WARNING) info = report.get_issues_by_severity(ValidationSeverity.INFO) if errors: lines.append("ERRORS:") for issue in errors: lines.append(f" ❌ {issue.message}") if issue.suggested_position: lines.append(f" → Suggested: {issue.suggested_position}") lines.append("") if warnings: lines.append("WARNINGS:") for issue in warnings: lines.append(f" ⚠️ {issue.message}") lines.append("") if info: lines.append("INFO:") for issue in info: lines.append(f" ℹ️ {issue.message}") lines.append("") # Corrected positions if report.corrected_positions: lines.append("CORRECTED POSITIONS:") for component_ref, (x, y) in report.corrected_positions.items(): lines.append(f" {component_ref}: ({x:.2f}, {y:.2f})") return "\n".join(lines) def export_validation_report(self, report: ValidationReport, filepath: str) -> None: """ Export validation report to JSON file. Args: report: ValidationReport to export filepath: Path to output file """ # Convert report to serializable format export_data = { "success": report.success, "total_components": report.total_components, "validated_components": report.validated_components, "out_of_bounds_count": report.out_of_bounds_count, "corrected_positions": report.corrected_positions, "issues": [ { "severity": issue.severity.value, "component_ref": issue.component_ref, "message": issue.message, "position": issue.position, "suggested_position": issue.suggested_position, "component_type": issue.component_type, } for issue in report.issues ], } with open(filepath, "w") as f: json.dump(export_data, f, indent=2)