Source code for circuit_synth.component_placement.placement

"""Component placement strategies for KiCad schematics.

This module provides placement algorithms for organizing components in a schematic.
"""

from dataclasses import dataclass
from typing import Dict, List, Optional, Set, Tuple

from .geometry import ComponentGeometryHandler, create_geometry_handler


[docs] @dataclass class PlacementNode: """Represents a component's placement information.""" component: "Component" x: float = 0 y: float = 0 rotation: float = 0 placed: bool = False connected_components: Set["Component"] = None geometry_handler: Optional[ComponentGeometryHandler] = None
[docs] def __post_init__(self): """Initialize after creation.""" if self.connected_components is None: self.connected_components = set() if self.geometry_handler is None: self.geometry_handler = create_geometry_handler( self.component.name, self.component.library )
[docs] class ComponentPlacer: """Handles intelligent placement of components in a schematic."""
[docs] def __init__(self, base_x: float = 77.47, base_y: float = 44.45): """Initialize the component placer.""" self.base_x = base_x self.base_y = base_y self.spacing_x = 2.54 # Horizontal spacing between components self.spacing_y = 2.54 # Vertical spacing between components self.placement_nodes: Dict[str, PlacementNode] = {} self.occupied_positions: List[Tuple[float, float, float, float]] = [] self.component_positions = {}
[docs] def analyze_connectivity(self, circuit: "Circuit") -> None: """Build a connectivity graph from the circuit nets.""" # Initialize placement nodes for each component self.placement_nodes = { comp.ref: PlacementNode(component=comp) for comp in circuit.components } # Analyze nets to build connectivity information for net in circuit.get_nets(): connected_components = set() for pin in net.pins: connected_components.add(pin.parent) # Update connectivity information for each component for comp in connected_components: node = self.placement_nodes[comp.ref] node.connected_components.update( c for c in connected_components if c != comp )
def _check_collision(self, node: PlacementNode) -> bool: """Check if component would collide with any placed components.""" bounds = node.geometry_handler.get_bounding_box(node.x, node.y, node.rotation) x1, y1, w1, h1 = bounds for x2, y2, w2, h2 in self.occupied_positions: # Check for overlap in both x and y if x1 < x2 + w2 and x1 + w1 > x2 and y1 < y2 + h2 and y1 + h1 > y2: return True return False def _find_non_colliding_position(self, node: PlacementNode) -> Tuple[float, float]: """Find a nearby position that doesn't collide with existing components.""" original_x, original_y = node.x, node.y offset = self.spacing_x spiral = 1 while True: # Try positions in a spiral pattern positions = [ (original_x + offset, original_y), # Right (original_x - offset, original_y), # Left (original_x, original_y + offset), # Down (original_x, original_y - offset), # Up ] for new_x, new_y in positions: node.x, node.y = new_x, new_y if not self._check_collision(node): return new_x, new_y offset += self.spacing_x spiral += 1 if spiral > 10: # Limit search to prevent infinite loops return original_x, original_y def _determine_component_rotation( self, node: PlacementNode, connected_to: List[Tuple["Component", str]] ) -> float: """Determine optimal component rotation based on connections.""" comp = node.component if comp.library == "power": return 0 # Power symbols always upright # For resistors, determine rotation based on connections if comp.name == "R": # Check for power connections first power_connections = [c for c, _ in connected_to if c.library == "power"] if power_connections: if any("+3V3" in c.name for c in power_connections): return 180 # Pin 1 faces up to 3V3 elif any("GND" in c.name for c in power_connections): return 0 # Pin 2 faces down to GND # For resistors in series, alternate orientations resistor_connections = [(c, p) for c, p in connected_to if c.name == "R"] if resistor_connections: # Get the connected resistor's reference number connected_ref_num = int( next(c.ref.replace("R", "") for c, _ in resistor_connections) ) # Get current resistor's reference number current_ref_num = int(comp.ref.replace("R", "")) # Alternate orientation based on position in chain return 180 if current_ref_num % 2 == 1 else 0 return 180 # Default to pin 1 up
[docs] def place_components( self, circuit: "Circuit" ) -> Dict[str, Tuple[float, float, float]]: """Place all components in the circuit and return their positions.""" self.analyze_connectivity(circuit) self.occupied_positions = [] # First pass: determine rotations and initial positions placed_components = {} # Group components by their connections net_groups = {} for net in circuit.get_nets(): connected = [] for pin in net.pins: connected.append((pin.parent, pin.number)) for comp, pin in connected: if comp.ref not in net_groups: net_groups[comp.ref] = [] net_groups[comp.ref].extend((c, p) for c, p in connected if c != comp) # Sort components by reference designator sorted_components = sorted( [comp for comp in circuit.components if comp.library != "power"], key=lambda x: (x.name, int(x.ref.replace(x.name, ""))), ) # Place non-power components first y_offset = 0 for comp in sorted_components: node = self.placement_nodes[comp.ref] connected_to = net_groups.get(comp.ref, []) # Determine rotation rotation = self._determine_component_rotation(node, connected_to) node.rotation = rotation # Place component node.x = self.base_x node.y = self.base_y + y_offset print(f"\nPlacing {comp.ref} at initial position ({node.x}, {node.y})") print(f"Component dimensions: {node.geometry_handler._dimensions}") print(f"Rotation: {rotation} degrees") # Check for collisions and adjust if needed if self._check_collision(node): print(f"Collision detected for {comp.ref}, finding new position...") node.x, node.y = self._find_non_colliding_position(node) print(f"New position for {comp.ref}: ({node.x}, {node.y})") # Update placement info node.placed = True placed_components[comp.ref] = (node.x, node.y, node.rotation) # Add to occupied positions bounds = node.geometry_handler.get_bounding_box( node.x, node.y, node.rotation ) self.occupied_positions.append(bounds) # Increment y offset for next component y_offset += self.spacing_y + node.geometry_handler._dimensions.height # Then place power components for comp in circuit.components: if comp.library == "power": node = self.placement_nodes[comp.ref] connected_to = net_groups.get(comp.ref, []) # Find the component this power symbol connects to connected_comp = next( (c for c, _ in connected_to if c.library != "power"), None ) if connected_comp and connected_comp.ref in placed_components: comp_pos = placed_components[connected_comp.ref] pin_number = next(p for c, p in connected_to if c == connected_comp) # Get connected component's geometry handler connected_node = self.placement_nodes[connected_comp.ref] # Calculate position relative to the connected component's pin pin_loc = connected_node.geometry_handler.get_pin_location( pin_number, comp_pos[2] ) if pin_loc: # Calculate absolute pin position of connected component abs_pin_x = comp_pos[0] + pin_loc[0] abs_pin_y = comp_pos[1] + pin_loc[1] print(f"\nPlacing power component {comp.ref}") print(f"Connected to {connected_comp.ref} at pin {pin_number}") print(f"Connected component pin at: ({abs_pin_x}, {abs_pin_y})") # Get power component's pin offset power_pin_loc = node.geometry_handler.get_pin_location("1", 0) print(f"Power component pin offset: {power_pin_loc}") # Place power component so its pin aligns with component pin x = abs_pin_x if "+3V3" in comp.name: y = ( abs_pin_y - power_pin_loc[1] ) # Align power pin with component pin print( f"Placing 3V3 at y={y} to align with pin at y={abs_pin_y}" ) else: # GND y = ( abs_pin_y - power_pin_loc[1] ) # Align power pin with component pin print( f"Placing GND at y={y} to align with pin at y={abs_pin_y}" ) node.x = x node.y = y node.rotation = 0 node.placed = True placed_components[comp.ref] = (x, y, 0) bounds = node.geometry_handler.get_bounding_box(x, y, 0) self.occupied_positions.append(bounds) # Store positions for wire routing self.component_positions = placed_components return placed_components