Source code for circuit_synth.core.net

# FILE: src/circuit_synth/core/net.py

from typing import Optional, Set, Any, Dict

from loguru import logger

from ._logger import context_logger
from .decorators import get_current_circuit
from .exception import CircuitSynthError
from .power_net_registry import is_power_net, get_power_symbol


[docs] class Net: """ A Net represents an electrical node (set of pins). Supports: - Automatic power net detection (GND, VCC, etc.) - Explicit power net declaration - Physical constraints (trace current, impedance) - Custom properties - Differential pairs (via KiCad naming conventions) Examples: # Auto-detected power net (recommended) >>> gnd = Net(name="GND") # Automatically becomes power net # Explicit power net >>> vcc = Net(name="VCC", is_power=True, power_symbol="power:VCC") # Prevent auto-detection >>> not_power = Net(name="GND_SENSE", is_power=False) # Differential pair (KiCad detects automatically by name) >>> usb_dp = Net(name="USB_DP", impedance=90) # KiCad pairs USB_DP/USB_DN >>> usb_dn = Net(name="USB_DN", impedance=90) # High current trace >>> power_5v = Net(name="+5V", trace_current=2000) # 2A, auto-detected as power # Custom properties >>> rf = Net(name="RF_OUT", impedance=50, substrate_height=1.6) Note: Differential pairs use KiCad naming conventions (_P/_N, +/-, etc.). KiCad automatically detects and routes them as differential pairs. """
[docs] def __init__( self, name: Optional[str] = None, # Power net parameters is_power: Optional[bool] = None, # None = auto-detect power_symbol: Optional[str] = None, # Physical constraints trace_current: Optional[float] = None, # mA impedance: Optional[float] = None, # ohms # Custom properties **properties: Any ): """ Create an electrical net. Args: name: Net name (e.g., "GND", "USB_DP"). Auto-generated if None. For differential pairs, use KiCad naming conventions: - NAME_P / NAME_N (e.g., USB_DP / USB_DN) - NAME+ / NAME- (e.g., ETH_TX+ / ETH_TX-) is_power: Power net flag. None (default) = auto-detect from name, True = explicitly mark as power net, False = explicitly NOT a power net. power_symbol: KiCad power symbol (e.g., "power:GND"). Auto-filled if is_power is auto-detected. trace_current: Maximum current in milliamps (mA). impedance: Target impedance in ohms for controlled impedance routing. For differential pairs, this is the differential impedance. **properties: Custom properties for specialized applications. Note: Differential pairs are detected automatically by KiCad based on net naming. Use matching prefixes with _P/_N, +/-, or similar suffixes. """ self.name = name self._pins: Set["Pin"] = set() # Auto-detect power nets if not explicitly specified if is_power is None and name: if is_power_net(name): is_power = True power_symbol = power_symbol or get_power_symbol(name) logger.debug( f"Auto-detected power net '{name}' -> {power_symbol}" ) self.is_power = is_power if is_power is not None else False self.power_symbol = power_symbol self.trace_current = trace_current self.impedance = impedance self.properties: Dict[str, Any] = properties # Validate at construction time self._validate_construction() # Immediately register with the current circuit circuit = get_current_circuit() if circuit is None: raise CircuitSynthError( f"Cannot create Net('{name or ''}'): No active circuit found." ) circuit.add_net(self)
def _validate_construction(self) -> None: """Validate parameters at construction time.""" # Power net validation if self.is_power and not self.power_symbol: raise CircuitSynthError( f"Power net '{self.name}' requires power_symbol parameter. " f"Example: Net(name='{self.name}', is_power=True, " f"power_symbol='power:GND')" ) # Physical constraint validation if self.trace_current is not None and self.trace_current <= 0: raise CircuitSynthError( f"trace_current must be positive, got {self.trace_current}" ) if self.impedance is not None and self.impedance <= 0: raise CircuitSynthError( f"impedance must be positive, got {self.impedance}" ) @property def pins(self): return frozenset(self._pins)
[docs] def __iadd__(self, other): """ net += pin => pin connects to this net net += net => unify (if different) by bringing other net's pins over """ from .pin import Pin if isinstance(other, Pin): other.connect_to_net(self) elif isinstance(other, Net): if other is not self: # unify: move all pins from 'other' into this net for p in list(other._pins): p.connect_to_net(self) else: raise TypeError(f"Cannot do net += with {type(other)}") return self
[docs] def to_dict(self) -> Dict[str, Any]: """Serialize Net to dictionary for JSON encoding.""" return { "name": self.name, "is_power": self.is_power, "power_symbol": self.power_symbol, "trace_current": self.trace_current, "impedance": self.impedance, "properties": self.properties, }
[docs] @staticmethod def from_dict(data: Dict[str, Any]) -> "Net": """ Deserialize Net from dictionary. Note: This creates a Net outside of circuit context, so it won't auto-register. Caller must manually add to circuit._nets. """ # Create Net with minimal circuit context handling # We'll manually construct it to avoid circuit registration net = object.__new__(Net) net.name = data.get("name") net._pins = set() net.is_power = data.get("is_power", False) net.power_symbol = data.get("power_symbol") net.trace_current = data.get("trace_current") net.impedance = data.get("impedance") net.properties = data.get("properties", {}) return net
def __repr__(self): nm = self.name if self.name else "unnamed" flags = [] if self.is_power: flags.append("power") if self.impedance is not None: flags.append(f"{self.impedance}Ω") flag_str = f" [{', '.join(flags)}]" if flags else "" return f"<Net {nm}{flag_str}>"