Source code for skidl.circuit

# -*- coding: utf-8 -*-

# The MIT License (MIT) - Copyright (c) Dave Vandenbout.

"""
Circuit management in SKiDL.

This module provides the Circuit class which serves as the central container
for all circuit elements (parts, nets, buses) and their interconnections. 
It handles hierarchical circuit structures, electrical rule checking (ERC),
and output generation (netlists, PCBs, SVGs, etc.).
"""

import builtins
import json
import subprocess
from collections import Counter, deque

import graphviz

try:
    from future import standard_library

    standard_library.install_aliases()
except ImportError:
    pass

from .bus import Bus
from .design_class import NetClasses, PartClasses
from .erc import dflt_circuit_erc
from .logger import active_logger, erc_logger, stop_log_file_output
from .net import NCNet, Net
from .node import Node
from .part import Part, PartUnit
from .pckg_info import __version__
from .pin import pin_types
from .schlib import SchLib
from .scriptinfo import get_script_dir, get_script_name, get_skidl_trace
from .skidlbaseobj import SkidlBaseObject
from .utilities import (
    detect_os,
    expand_buses,
    export_to_all,
    flatten,
    num_to_chars,
    opened,
    reset_get_unique_name,
)


[docs] @export_to_all class Circuit(SkidlBaseObject): """ Container for an entire electronic circuit design. The Circuit class is the central repository for all the parts, nets, buses, and interfaces that make up a circuit. It manages the hierarchical structure of the design, performs electrical rule checking, and generates various outputs like netlists, PCB files, and graphical representations. Attributes: parts (list): List of all parts in the circuit. nets (list): List of all nets in the circuit. buses (list): List of all buses in the circuit. interfaces (list): List of all interfaces in the circuit. hierarchy (str): Current position in the hierarchy, represented as a dot-separated string. level (int): Current level in the hierarchy. context (list): Stack tracking the context at each hierarchical level. erc_list (list): List of ERC (Electrical Rule Checking) functions to run on the circuit. NC (Net): The special no-connect net used in this circuit. """ # Set the default ERC functions for all Circuit instances. erc_list = [dflt_circuit_erc] def __init__(self, **attrs): """ Initialize a new Circuit object. Args: **attrs: Arbitrary keyword arguments to set as attributes of the circuit. """ super().__init__() # Store the directory of the top-level script when this Circuit is first created. self.script_dir = get_script_dir() self.track_src = True # By default, put track source info into outputs like netlists. self.track_abs_path = False # By default, track using relative paths. self.reset(init=True) # Set passed-in attributes for the circuit. for k, v in list(attrs.items()): setattr(self, k, v) def __iadd__(self, *stuff): """ Add parts, nets, buses, and interfaces to the circuit. Args: *stuff: Various circuit elements to add. Returns: Circuit: The updated circuit with new elements. """ return self.add_stuff(*stuff) def __isub__(self, *stuff): """ Remove parts, nets, buses, and interfaces from the circuit. Args: *stuff: Various circuit elements to remove. Returns: Circuit: The updated circuit with elements removed. """ return self.rmv_stuff(*stuff) def __enter__(self): """ When entering a Circuit context, save the previous default_circuit and make this circuit the default_circuit. Returns: Circuit: The new default circuit instance. """ self.circuit_stack.append(default_circuit) builtins.default_circuit = self builtins.NC = self.NC return self def __exit__(self, type, value, traceback): """ Exit the current Circuit context and restore the previous one as the default_circuit. Args: type: Exception type if an exception occurred. value: Exception value if an exception occurred. traceback: Traceback if an exception occurred. """ builtins.default_circuit = self.circuit_stack.pop() builtins.NC = default_circuit.NC
[docs] def mini_reset(self, init=False): """Reset the circuit to its initial state while preserving certain attributes. This method reinitializes most circuit attributes to their default values, including clearing parts, nets, buses, interfaces, and design classes. It also resets counters and creates a new no-connect net. Args: init (bool, optional): If True, indicates this is being called during initialization. If False (default), additional setup is performed for the default circuit's global no-connect net. Note: - The circuit_stack is only initialized if it doesn't already exist - When init=False and this is the default circuit, the global NC (no-connect) net is updated - All unique name heaps are cleared via reset_get_unique_name() """ self.group_name_cntr = Counter() self.name = "" self.parts = [] self.nets = [] self.buses = [] self.interfaces = [] self._netclasses = NetClasses() self._partclasses = PartClasses() self.nodes = set() # Set of all nodes in the circuit hierarchy. self.active_node = None # Serves as the null parent of the root. self.root = self.activate(Node(name="", tag="", circuit=self)) if not hasattr(self, "circuit_stack"): # Initialize the circuit stack if it doesn't exist. # Otherwise, leave it alone since we might need to get back to the # parent Circuit object. self.circuit_stack = deque() self.erc_assertion_list = [] self.no_files = False # Allow creation of files for netlists, ERC, libs, etc. # Clear the name heap for nets, parts, etc. reset_get_unique_name() # Clear out the no-connect net and set the global no-connect if it's # tied to this circuit. self.NC = NCNet(name="__NOCONNECT", circuit=self) if not init and self is default_circuit: builtins.NC = self.NC
[docs] def reset(self, init=False): """ Clear any circuitry and cached part libraries and start over. Args: init (bool, optional): True if this is being called during initialization. """ try: from . import skidl config = skidl.config except ImportError: # For Python 2. Always gotta be different... from .skidl import config # Clear circuitry. self.mini_reset(init) # Also clear any cached libraries. SchLib.reset() # Clear out any old backup lib so the new one will get reloaded when it's needed. config.backup_lib = None
[docs] def get_node_names(self): """ Get the hierarchical names of all subcircuits/groups in the hierarchy. Returns: tuple: Tuple of hierarchical node names in the circuit hierarchy. """ return (node.hiertuple for node in self.nodes)
[docs] def activate(self, node): """ Activate a node in the circuit hierarchy. This method adds the given node to the circuit's node set, establishes parent-child relationships in the hierarchy, and sets it as the current active node. Args: node (Node): The node instance to activate. Returns: Node: The activated node instance. Note: If an active node already exists, the new node becomes its child. The new node then becomes the active node for subsequent operations. """ self.nodes.add(node) if self.active_node: self.active_node.add_child(node) self.active_node = node return node
[docs] def deactivate(self): """ Deactivate the current hierarchical node and return to its parent. This method moves up one level in the circuit hierarchy by setting the active node to its parent. The circuitry created within the current hierarchical level remains intact in the part and net lists, but the hierarchical context is restored to the previous level. Returns: None Note: This method does not remove any circuit components or connections that were added while the current node was active. """ # Restore the hierarchy that existed before this one was created. # This does not remove the circuitry since it has already been # added to the part and net lists. self.active_node = self.active_node.parent
[docs] def add_netclasses(self, *netclasses): """ Add one or more net classes to the circuit. Args: *netclasses: One or more net classes to add to the circuit. """ self._netclasses.add(netclasses)
@property def netclasses(self): """ Get the netclasses dictionary for this circuit. Returns: dict: A dictionary containing the netclasses defined for this circuit. """ return self._netclasses @netclasses.setter def netclasses(self, *netclasses): """ Add net classes for the circuit. Args: *netclasses: One or more net classes to add to the circuit. """ self.add_netclasses(netclasses) @netclasses.deleter def netclasses(self): """ Clear all net classes from the circuit. This removes all netclass definitions from the circuit, clearing the netclasses dictionary. """ self._netclasses = NetClasses()
[docs] def add_partclasses(self, *partclasses): """ Add one or more part classes to the circuit. Args: *partclasses: One or more part classes to add to the circuit. """ self._partclasses.add(partclasses)
@property def partclasses(self): """ Get the dictionary of part classes available in this circuit. Returns: dict: A dictionary mapping part class names to their corresponding part class objects. """ return self._partclasses @partclasses.setter def partclasses(self, *partclasses): """ Add part classes for the circuit. Args: *partclasses: One or more part classes to add to the circuit. """ self.add_partclasses(partclasses) @partclasses.deleter def partclasses(self): """ Clear all part classes from the circuit. This removes all partclass definitions from the circuit, clearing the partclasses dictionary. """ self._partclasses = PartClasses()
[docs] def add_parts(self, *parts): """ Add parts to the circuit. Args: *parts: Part objects to add to the circuit. Raises: ValueError: If attempting to add an unmovable part. """ for part in parts: # Add the part to this circuit if the part is movable and # it's not already in this circuit. if part.circuit != self: if part.is_movable(): # Remove the part from the circuit it's already in. if isinstance(part.circuit, Circuit): part.circuit -= part # Add the part to this circuit. part.circuit = self # Record the Circuit object for this part. part.ref = part.ref # Adjusts the part reference if necessary. # Add the part to the currently active node. self.active_node.parts.append(part) part.node = self.active_node # Store part instantiation trace. part.skidl_trace = get_skidl_trace() self.parts.append(part) else: active_logger.raise_( ValueError, f"Can't add unmovable part {part.ref} to this circuit.", )
[docs] def rmv_parts(self, *parts): """ Remove parts from the circuit. Args: *parts: Part objects to remove from the circuit. Raises: ValueError: If attempting to remove an unmovable part. """ for part in parts: part.disconnect() if part.is_movable(): if part.circuit == self and part in self.parts: part.node.parts.remove(part) part.node = None part.circuit = None part.hierarchy = None self.parts.remove(part) else: active_logger.warning( f"Removing non-existent part {part.ref} from this circuit." ) else: active_logger.raise_( ValueError, f"Can't remove part {part.ref} from this circuit.", )
[docs] def add_nets(self, *nets): """ Add nets to the circuit. Args: *nets: Net objects to add to the circuit. Raises: ValueError: If attempting to add an unmovable net. """ for net in nets: # Add the net to this circuit if the net is movable and # it's not already in this circuit. if net.circuit != self: if net.is_movable(): # Remove the net from the circuit it's already in. if isinstance(net.circuit, Circuit): net.circuit -= net # Save the Circuit object the net belongs to. net.circuit = self # Self-reassign to make the net name unique. net.name = net.name # Add the net to the currently active node. self.active_node.nets.append(net) net.node = self.active_node # Store net instantiation trace. net.skidl_trace = get_skidl_trace() self.nets.append(net) else: active_logger.raise_( ValueError, f"Can't add unmovable net {net.name} to this circuit.", )
[docs] def rmv_nets(self, *nets): """ Remove nets from the circuit. Args: *nets: Net objects to remove from the circuit. Raises: ValueError: If attempting to remove an unmovable net. """ for net in nets: if net.is_movable(): if net.circuit == self and net in self.nets: net.node.nets.remove(net) net.node = None net.circuit = None net.hierarchy = None self.nets.remove(net) else: active_logger.warning( f"Removing non-existent net {net.name} from this circuit." ) else: active_logger.raise_( ValueError, f"Can't remove unmovable net {net.name} from this circuit.", )
[docs] def add_buses(self, *buses): """ Add buses to the circuit. Args: *buses: Bus objects to add to the circuit. Raises: ValueError: If attempting to add an unmovable bus. """ for bus in buses: # Add the bus to this circuit if the bus is movable and # it's not already in this circuit. if bus.circuit != self: if bus.is_movable(): # Remove the bus from the circuit it's already in, but skip # this if the bus isn't already in a Circuit. if isinstance(bus.circuit, Circuit): bus.circuit -= bus # Save the Circuit object the bus belongs to. bus.circuit = self # Self-reassign to make the bus name unique. bus.name = bus.name # Add the bus to the currently active node. self.active_node.buses.append(bus) bus.node = self.active_node # Store bus instantiation trace. bus.skidl_trace = get_skidl_trace() # Add the bus to the circuit. self.buses.append(bus) # Add the individual bus nets to the circuit. for net in bus.nets: self += net
[docs] def rmv_buses(self, *buses): """ Remove buses from the circuit. Args: *buses: Bus objects to remove from the circuit. Raises: ValueError: If attempting to remove an unmovable bus. """ for bus in buses: if bus.is_movable(): if bus.circuit == self and bus in self.buses: bus.node.buses.remove(bus) bus.node = None bus.circuit = None bus.hierarchy = None self.buses.remove(bus) for net in bus.nets: self -= net else: active_logger.warning( f"Removing non-existent bus {bus.name} from this circuit." ) else: active_logger.raise_( ValueError, f"Can't remove unmovable bus {bus.name} from this circuit.", )
[docs] def add_stuff(self, *stuff): """ Add various circuit elements to the circuit. Args: *stuff: Parts, nets, buses, or interfaces to add. Returns: Circuit: The updated circuit. Raises: ValueError: If attempting to add an unsupported type. """ for thing in flatten(stuff): if isinstance(thing, Part): self.add_parts(thing) elif isinstance(thing, Net): self.add_nets(thing) elif isinstance(thing, Bus): self.add_buses(thing) else: active_logger.raise_( ValueError, f"Can't add a {type(thing)} to a Circuit object.", ) return self
[docs] def rmv_stuff(self, *stuff): """ Remove various circuit elements from the circuit. Args: *stuff: Parts, nets, buses, or interfaces to remove. Returns: Circuit: The updated circuit. Raises: ValueError: If attempting to remove an unsupported type. """ for thing in flatten(stuff): if isinstance(thing, Part): self.rmv_parts(thing) elif isinstance(thing, Net): self.rmv_nets(thing) elif isinstance(thing, Bus): self.rmv_buses(thing) else: active_logger.raise_( ValueError, f"Can't remove a {type(thing)} from a Circuit object.", ) return self
[docs] def get_nets(self): """ Get all distinct nets in the circuit. This excludes the no-connect net, empty nets, and nets that are electrically connected to other nets already in the result list. Returns: list: List of distinct nets in the circuit. """ distinct_nets = [] for net in self.nets: if net is self.NC: # Exclude no-connect net. continue if not net.pins: # Exclude empty nets with no attached pins. continue for n in distinct_nets: # Exclude net if its already attached to a previously selected net. if net.is_attached(n): break else: # This net is not attached to any of the other distinct nets, # so it is also distinct. distinct_nets.append(net) return distinct_nets
[docs] def merge_net_names(self): """ Assign the same name to all segments of multi-segment nets. This ensures that connected nets share a common name. """ for net in self.nets: if len(net.nets) > 1: net.merge_names()
[docs] def merge_nets(self): """ Merge multi-segment nets into a single net. Note: Multi-segment nets had to be merged or else tests to detect the same net would fail in routing.py when generating schematics. But as a result of merging, net variables can become invalid because of new merging. Therefore, only do this when generating schematics so other generate_*() functions will not be affected. """ merged_nets = set() for net in self.nets: if len(net.nets) > 1 and net not in merged_nets: # Select a single name for the net segments. net.merge_names() # Record merged nets so they aren't processed again. merged_nets.update(set(net.nets) - {net}) # Move all pins to a single segment. for pin in net.pins: pin.move(net) # Remove merged nets from the circuit. self.nets = list(set(self.nets) - merged_nets)
[docs] def ERC(self, *args, **kwargs): """ Perform Electrical Rule Checking on the circuit. This method runs both the class-wide ERC functions and any local ERC functions defined for this circuit. It checks for issues like unconnected pins, pin type conflicts, etc. Args: *args: Arguments to pass to the ERC functions. **kwargs: Keyword arguments to pass to the ERC functions. """ # Save the currently active logger and activate the ERC logger. active_logger.push(erc_logger) # Reset the counters to clear any warnings/errors from previous ERC run. active_logger.error.reset() active_logger.warning.reset() self.merge_net_names() if self.no_files: active_logger.stop_file_output() super().ERC(*args, **kwargs) active_logger.report_summary("running ERC") # Restore the logger that was active before the ERC. active_logger.pop()
[docs] def cull_unconnected_parts(self): """ Remove parts that aren't connected to anything in the circuit. This can be useful to clean up a design by removing unused parts. """ for part in self.parts: if not part.is_connected(): self -= part
[docs] def check_for_empty_footprints(self): """ Check that all parts have assigned footprints. This method calls the empty_footprint_handler for any parts that don't have a footprint assigned. """ for part in self.parts: if getattr(part, "footprint", "") == "": import skidl skidl.empty_footprint_handler(part)
[docs] def check_tags(self): """ Check for missing part or hierarchical node tags. Tags are important for maintaining stable associations between schematic parts and PCB footprints. This method warns about any missing tags. """ for part in self.parts: part.check_tag(create_if_missing=True) for node in self.nodes: node.check_tag(create_if_missing=False)
[docs] def generate_netlist(self, **kwargs): """ Generate a netlist for the circuit. Args: file_ (str or file object, optional): File to write netlist to. tool (str, optional): The EDA tool to generate the netlist for. do_backup (bool, optional): If True, create a library with all parts in the circuit. **kwargs: Additional arguments passed to the tool-specific netlist generator. Returns: str: The generated netlist as a string. """ from . import skidl from .tools import tool_modules # Reset the counters to clear any warnings/errors from previous run. active_logger.error.reset() active_logger.warning.reset() self.merge_net_names() # Don't do any checks for empty footprints or missing tags # since this should be done when tool-specific netlists are generated. # For example, these checks aren't necessary when generating # a SPICE netlist. # Extract arguments: # Get EDA tool the netlist will be generated for. # Get file the netlist will be stored in (if any). # Get flag controlling the generation of a backup library. tool = kwargs.pop("tool", skidl.config.tool) file_ = kwargs.pop("file_", None) do_backup = kwargs.pop("do_backup", True) # Pass these settings from the Circuit object if they are not already set. kwargs["track_abs_path"] = kwargs.get("track_abs_path", self.track_abs_path) kwargs["track_src"] = kwargs.get("track_src", self.track_src) netlist = tool_modules[tool].gen_netlist(self, **kwargs) active_logger.report_summary("generating netlist") if not self.no_files: with opened(file_ or (get_script_name() + ".net"), "w") as f: f.write(str(netlist)) if do_backup: self.backup_parts() # Create a new backup lib for the circuit parts. # Clear out any old backup lib so the new one will get reloaded when it's needed. skidl.config.backup_lib = None return netlist
[docs] def generate_pcb(self, **kwargs): """ Create a PCB file from the circuit. Args: file_ (str or file object, optional): File to write PCB data to. tool (str, optional): The EDA tool to generate the PCB for. do_backup (bool, optional): If True, create a library with all parts in the circuit. fp_libs (list, optional): List of directories containing footprint libraries. **kwargs: Additional arguments passed to the tool-specific PCB generator. """ from . import skidl from .tools import tool_modules # Reset the counters to clear any warnings/errors from previous run. active_logger.error.reset() active_logger.warning.reset() self.merge_net_names() # Check for things that can cause problems. self.check_for_empty_footprints() self.check_tags() # Extract arguments: # Get EDA tool the netlist will be generated for. # Get file the netlist will be stored in (if any). # Get flag controlling the generation of a backup library. # Get list of footprint libraries. tool = kwargs.pop("tool", skidl.config.tool) file_ = kwargs.pop("file_", None) do_backup = kwargs.pop("do_backup", True) fp_libs = kwargs.pop("fp_libs", None) if not self.no_files: if do_backup: self.backup_parts() # Create a new backup lib for the circuit parts. # Clear out any old backup lib so the new one will get reloaded when it's needed. skidl.config.backup_lib = None tool_modules[tool].gen_pcb(self, file_, fp_libs=fp_libs) active_logger.report_summary("creating PCB")
[docs] def generate_xml(self, file_=None, tool=None): """ Generate an XML representation of the circuit. Args: file_ (str or file object, optional): File to write XML data to. tool (str, optional): Backend tool to use for XML generation. Returns: str: The generated XML as a string. """ from . import skidl from .tools import tool_modules # Reset the counters to clear any warnings/errors from previous run. active_logger.error.reset() active_logger.warning.reset() self.merge_net_names() tool = tool or skidl.config.tool netlist = tool_modules[tool].gen_xml(self) if not self.no_files: with opened(file_ or (get_script_name() + ".xml"), "w") as f: f.write(netlist) active_logger.report_summary("generating XML") return netlist
[docs] def generate_netlistsvg_skin(self, net_stubs, layout_options=None): """ Generate SVG for schematic symbols for a netlistsvg skin file. This creates the SVG symbol definitions that will be used by netlistsvg to visualize the circuit. Args: net_stubs (list): List of nets that are stubbed rather than routed. layout_options (str, optional): String of ELK layout options. Defaults to None. See https://eclipse.dev/elk/reference/options.html Returns: str: SVG content for the skin file. """ default_layout_options = """ org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers="5" org.eclipse.elk.layered.compaction.postCompaction.strategy="4" org.eclipse.elk.spacing.nodeNode="50" org.eclipse.elk.direction="DOWN" """ layout_options = layout_options or default_layout_options # Generate the SVG for each part in the required transformations. part_svg = {} for part in self.parts: # If this part is attached to any net stubs, give it a symbol # name specifically for this part + stubs. if part.attached_to(net_stubs): # This part is attached to net stubs, so give it # a symbol name specifically for this part + stubs. symbol_name = part.name + "_" + part.ref else: # This part is not attached to any stubs, so give it # a symbol name for this generic part symbol. symbol_name = part.name # Get the global transformation for the part symbol. if len(part.unit) == 1: # If there is only a single unit, then that unit is the same as the part. # Therefore, ignore the global transformation for the part since we'll # pick it up from the unit transformation instead. global_symtx = "" else: # If there are zero units or more than one unit, then use the # global transformation for the part or apply it to the units. global_symtx = getattr(part, "symtx", "") # Get the transformation for each part unit (if any). unit_symtx = set([""]) for unit in part.unit.values(): unit_symtx.add(getattr(unit, "symtx", "")) # Each combination of global + unit transformation is one of # the total transformations needed for the part. total_symtx = [global_symtx + u_symtx for u_symtx in unit_symtx] # Generate SVG of the part for each total transformation. for symtx in total_symtx: name = symbol_name + "_" + symtx # Skip any repeats of the part. if name not in part_svg.keys(): part_svg[name] = part.generate_svg_component( symtx=symtx, net_stubs=net_stubs ) part_svg = list(part_svg.values()) # Just keep the SVG for the part symbols. head_svg = [ '<svg xmlns="http://www.w3.org/2000/svg"', ' xmlns:xlink="http://www.w3.org/1999/xlink"', ' xmlns:s="https://github.com/nturley/netlistsvg">', " <s:properties", ' constants="false"', ' splitsAndJoins="false"', ' genericsLaterals="true">', " <s:layoutEngine {layout_options} />", " </s:properties>", "<style>", "svg {{", " stroke: #000;", " fill: none;", " stroke-linejoin: round;", " stroke-linecap: round;", "}}", "text {{", " fill: #000;", " stroke: none;", " font-size: 10px;", " font-weight: bold;", ' font-family: "Courier New", monospace;', "}}", ".skidl_text {{", " fill: #999;", " stroke: none;", " font-weight: bold;", ' font-family: consolas, "Courier New", monospace;', "}}", ".pin_num_text {{", " fill: #840000;", "}}", ".pin_name_text {{", " fill: #008484;", "}}", ".net_name_text {{", " font-style: italic;", " fill: #840084;", "}}", ".part_text {{", " fill: #840000;", "}}", ".part_ref_text {{", " fill: #008484;", "}}", ".part_name_text {{", " fill: #008484;", "}}", ".pen_fill {{", " fill: #840000;", "}}", ".background_fill {{", " fill: #FFFFC2", "}}", ".nodelabel {{", " text-anchor: middle;", "}}", ".inputPortLabel {{", " text-anchor: end;", "}}", ".splitjoinBody {{", " fill: #000;", "}}", ".symbol {{", " stroke-linejoin: round;", " stroke-linecap: round;", " stroke: #840000;", "}}", ".detail {{", " stroke-linejoin: round;", " stroke-linecap: round;", " fill: #000;", "}}", "</style>", "", "<!-- signal -->", '<g s:type="inputExt" s:width="30" s:height="20" transform="translate(0,0)">', ' <text x="-2" y="12" text-anchor=\'end\' class="$cell_id pin_name_text" s:attribute="ref">input</text>', ' <s:alias val="$_inputExt_"/>', ' <path d="M0,0 V20 H15 L30,10 15,0 Z" class="$cell_id symbol"/>', ' <g s:x="30" s:y="10" s:pid="Y" s:position="right"/>', "</g>", "", '<g s:type="outputExt" s:width="30" s:height="20" transform="translate(0,0)">', ' <text x="32" y="12" class="$cell_id pin_name_text" s:attribute="ref">output</text>', ' <s:alias val="$_outputExt_"/>', ' <path d="M30,0 V20 H15 L0,10 15,0 Z" class="$cell_id symbol"/>', ' <g s:x="0" s:y="10" s:pid="A" s:position="left"/>', "</g>", "<!-- signal -->", "", "<!-- builtin -->", '<g s:type="generic" s:width="30" s:height="40" transform="translate(0,0)">', ' <text x="15" y="-4" class="nodelabel $cell_id" s:attribute="ref">generic</text>', ' <rect width="30" height="40" x="0" y="0" s:generic="body" class="$cell_id"/>', ' <g transform="translate(30,10)"', ' s:x="30" s:y="10" s:pid="out0" s:position="right">', ' <text x="5" y="-4" class="$cell_id">out0</text>', " </g>", ' <g transform="translate(30,30)"', ' s:x="30" s:y="30" s:pid="out1" s:position="right">', ' <text x="5" y="-4" class="$cell_id">out1</text>', " </g>", ' <g transform="translate(0,10)"', ' s:x="0" s:y="10" s:pid="in0" s:position="left">', ' <text x="-3" y="-4" class="inputPortLabel $cell_id">in0</text>', " </g>", ' <g transform="translate(0,30)"', ' s:x="0" s:y="30" s:pid="in1" s:position="left">', ' <text x="-3" y="-4" class="inputPortLabel $cell_id">in1</text>', " </g>", "</g>", "<!-- builtin -->", ] head_svg = "\n".join(head_svg).format(**locals()) part_svg = "\n".join(part_svg) tail_svg = "</svg>" return "\n".join((head_svg, part_svg, tail_svg))
[docs] def get_net_nc_stubs(self): """ Get all nets/buses that are stubs or no-connects. Returns: list: List of nets that are stubs or no-connects. """ # Search all nets for those set as stubs or that are no-connects. stubs = [ n for n in self.nets if getattr(n, "stub", False) or isinstance(n, NCNet) ] # Also find buses that are set as stubs and add their individual nets. stubs.extend(expand_buses([b for b in self.buses if getattr(b, "stub", False)])) return stubs
[docs] def generate_svg(self, file_=None, tool=None, layout_options=None): """ Create an SVG visualization of the circuit and return the netlistsvg input data. Args: file_ (str, optional): Base filename to store SVG and intermediate files. tool (str, optional): Backend tool to use. layout_options (str, optional): Options to control netlistsvg/ELK layout algorithm. Returns: dict: JSON dictionary that can be used as input to netlistsvg. """ # Reset the counters to clear any warnings/errors from previous run. active_logger.error.reset() active_logger.warning.reset() self.merge_net_names() # Get the list of nets which will be routed and not represented by stubs. net_stubs = self.get_net_nc_stubs() routed_nets = list(set(self.nets) - set(net_stubs)) # Assign each routed net a unique integer. Interconnected nets # all get the same number. net_nums = {} for num, net in enumerate(routed_nets, 1): for n in net.get_nets(): if n.name not in net_nums: net_nums[n.name] = num io_dict = {"i": "input", "o": "output", "n": "nc"} # Assign I/O ports to any named net that has a netio attribute. ports = {} for net in routed_nets: if not net.is_implicit(): try: # Net I/O direction set by 1st letter of netio attribute. io = io_dict[net.netio.lower()[0]] ports[net.name] = { "direction": io, "bits": [ net_nums[net.name], ], } except AttributeError: # Net has no netio so don't assign a port. pass pin_dir_tbl = { pin_types.INPUT: "input", pin_types.OUTPUT: "output", pin_types.BIDIR: "output", pin_types.TRISTATE: "output", pin_types.PASSIVE: "input", pin_types.PULLUP: "output", pin_types.PULLDN: "output", pin_types.UNSPEC: "input", pin_types.PWRIN: "input", pin_types.PWROUT: "output", pin_types.OPENCOLL: "output", pin_types.OPENEMIT: "output", pin_types.NOCONNECT: "nc", } cells = {} for part in self.parts: if part.attached_to(net_stubs): # If the part is attached to any net stubs, it will require # an individualized symbol in the netlistsvg skin file. # Give it a unique symbol name so it can be found later. part_name = part.name + "_" + part.ref else: # Otherwise, no net stubs so use the name of the # generic symbol for this part. part_name = part.name part_symtx = getattr(part, "symtx", "") # Get a list of the units in the part. if len(part.unit) <= 1: # If there are no units, then use the part itself as a unit. # If there is only one unit, then that unit is just the part itself, so use it. units = [part] else: units = part.unit.values() for unit in units: if not unit.is_connected(): continue # Skip unconnected parts. pins = unit.get_pins() # Associate each connected pin of a part with the assigned net number. connections = { pin.num: [ net_nums[pin.net.name], ] for pin in pins if pin.net in routed_nets } # Assign I/O to each part pin by either using the pin's symio # attribute or by using its pin function. part_pin_dirs = { pin.num: io_dict[ getattr(pin, "symio", pin_dir_tbl[pin.func]).lower()[0] ] for pin in pins } # Keep only input/output pins. Remove no-connect pins. part_pin_dirs = {n: d for n, d in part_pin_dirs.items() if d[0] in "io"} # Determine which symbol in the skin file goes with this part. unit_symtx = part_symtx + getattr(unit, "symtx", "") if not isinstance(unit, PartUnit): ref = part.ref name = part_name + "_1_" + part_symtx else: ref = part.ref + num_to_chars(unit.num) name = part_name + "_" + str(unit.num) + "_" + unit_symtx # Create the cell that netlistsvg uses to draw the part and connections. cells[ref] = { "type": name, "port_directions": part_pin_dirs, "connections": connections, "attributes": { "value": str(part.value), }, } schematic_json = { "modules": { self.name: { "ports": ports, "cells": cells, } } } if not self.no_files: file_basename = file_ or get_script_name() json_file = file_basename + ".json" svg_file = file_basename + ".svg" with opened(json_file, "w") as f: f.write( json.dumps( schematic_json, sort_keys=True, indent=2, separators=(",", ": ") ) ) skin_file = file_basename + "_skin.svg" with opened(skin_file, "w") as f: f.write( self.generate_netlistsvg_skin( net_stubs=net_stubs, layout_options=layout_options ) ) if detect_os() == "Windows": subprocess.Popen( ["netlistsvg.cmd", json_file, "--skin", skin_file, "-o", svg_file], shell=False, ) else: subprocess.Popen( ["netlistsvg", json_file, "--skin", skin_file, "-o", svg_file], shell=False, ) active_logger.report_summary("generating SVG") return schematic_json
[docs] def generate_schematic(self, **kwargs): """ Create a schematic file from the circuit. This generates a visual representation of the circuit that can be opened in an EDA tool like KiCad's Eeschema. Args: **kwargs: Arguments for the schematic generator including: empty_footprint_handler (function, optional): Custom handler for parts without footprints. tool (str, optional): The EDA tool to generate the schematic for. """ import skidl from .tools import tool_modules # Reset the counters to clear any warnings/errors from previous run. active_logger.error.reset() active_logger.warning.reset() # Supply a schematic-specific empty footprint handler. save_empty_footprint_handler = skidl.empty_footprint_handler def _empty_footprint_handler(part): """Handle the situation of a Part with no footprint when generating a schematic.""" active_logger.warning( f"No footprint for {part.name}/{part.ref}." ) # Supply a nonsense footprint just so no complaints are raised when the EESCHEMA code is generated. part.footprint = ":" if kwargs.get("empty_footprint_handler]"): skidl.empty_footprint_handler = kwargs["empty_footprint_handler"] else: skidl.empty_footprint_handler = _empty_footprint_handler self.merge_net_names() self.merge_nets() # Merge nets or schematic routing will fail. tool = kwargs.pop("tool", skidl.config.tool) try: tool_modules[tool].gen_schematic(self, **kwargs) finally: skidl.empty_footprint_handler = save_empty_footprint_handler active_logger.report_summary("generating schematic")
[docs] def generate_dot( self, file_=None, engine="neato", rankdir="LR", part_shape="rectangle", net_shape="point", splines=None, show_values=True, show_anon=False, split_nets=["GND"], split_parts_ref=[], ): """ Generate a Graphviz DOT visualization of the circuit. Creates a graphical representation of the circuit as a graph where parts and nets are nodes, and connections are edges. Args: file_ (str, optional): File to write the DOT data to. engine (str, optional): Graphviz layout engine to use. Default is "neato". rankdir (str, optional): Direction of graph layout. Default is "LR" (left to right). part_shape (str, optional): Shape to use for part nodes. Default is "rectangle". net_shape (str, optional): Shape to use for net nodes. Default is "point". splines (str, optional): Style for the edges. Try "ortho" for schematic-like feel. show_values (bool, optional): Show part values as labels. Default is True. show_anon (bool, optional): Show anonymous net names. Default is False. split_nets (list, optional): List of net names to split in visualization. Default is ["GND"]. split_parts_ref (list, optional): List of part references to split in visualization. Returns: graphviz.Digraph: A Graphviz graph object. """ # Reset the counters to clear any warnings/errors from previous run. active_logger.error.reset() active_logger.warning.reset() self.merge_net_names() dot = graphviz.Digraph(engine=engine) dot.attr(rankdir=rankdir, splines=splines) nets = self.get_nets() # try and keep things in the same order nets.sort(key=lambda n: n.name.lower()) # Add stubbed nets to split_nets: split_nets = split_nets[:] # Make a local copy. split_nets.extend([n.name for n in nets if getattr(n, "stub", False)]) for i, n in enumerate(nets): xlabel = n.name if not show_anon and n.is_implicit(): xlabel = None if n.name not in split_nets: dot.node(n.name, shape=net_shape, xlabel=xlabel) for j, pin in enumerate(n.pins): net_ref = n.name pin_part_ref = pin.part.ref if n.name in split_nets: net_ref += str(j) dot.node(net_ref, shape=net_shape, xlabel=xlabel) if pin.part.ref in split_parts_ref and n.name not in split_nets: label = pin.part.ref + ":" + pin.name # add label to part net_ref_part = "%s_%i_%i" % (net_ref, i, j) dot.node(net_ref_part, shape=net_shape, xlabel=label) dot.edge(pin_part_ref, net_ref_part, arrowhead="none") # add label to splited net pin_part_ref = "%s_%i_%i" % (pin_part_ref, i, j) dot.node(pin_part_ref, shape=net_shape, xlabel=label) dot.edge(pin_part_ref, net_ref, arrowhead="none") else: dot.edge( pin_part_ref, net_ref, arrowhead="none", taillabel=pin.name ) for p in sorted(self.parts, key=lambda p: p.ref.lower()): xlabel = None if show_values: xlabel = str(p.value) dot.node(p.ref, shape=part_shape, xlabel=xlabel) if not self.no_files: if file_ is not None: dot.save(file_) active_logger.report_summary("generating DOT") return dot
generate_graph = generate_dot # Old method name for generating graphviz dot file.
[docs] def backup_parts(self, file_=None): """ Save all parts in the circuit as a SKiDL library file. This creates a backup library that can be used to restore the parts in the circuit. Args: file_ (str or file object, optional): File to write the library to. If None, a standard library file will be used. """ from . import skidl from skidl import SKIDL if self.no_files: return self.merge_net_names() lib = SchLib(tool=SKIDL) # Create empty library. for p in self.parts: lib += p file_ = file_ or skidl.config.backup_lib_file_name lib.export(libname=skidl.config.backup_lib_name, file_=file_)
[docs] def to_tuple(self): """ Create a nested tuple representation of the circuit for comparison. The tuple contains sorted tuples of parts and nets information, suitable for comparing circuits structurally. Returns: tuple: A tuple containing: - A tuple of part representations (ref, name, lib) - A tuple of net representations (name, pins) """ self.merge_net_names() return ( tuple( sorted( ( (p.ref, p.name, p.lib.filename) for p in self.parts ), key=lambda x: x[0].lower(), ) ), tuple( sorted( ( (n.name, tuple(sorted(tuple((p.part.ref, p.num) for p in n.get_pins())))) for n in self.get_nets() ), key=lambda x: x[0].lower(), ) ), )
@property def no_files(self): """ Control whether output files are generated. Returns: bool: True if file output is suppressed, False otherwise. """ return self._no_files @no_files.setter def no_files(self, stop): """ Set whether to suppress file output. Args: stop (bool): True to suppress file output, False to allow it. """ self._no_files = stop stop_log_file_output(stop)