Source code for skidl.circuit

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

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

"""
Handles complete circuits made of parts and nets.
"""

from __future__ import (  # isort:skip
    absolute_import,
    division,
    print_function,
    unicode_literals,
)

import functools
import json
import re
import subprocess
import time
from builtins import range, str, super
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 .common import builtins
from .erc import dflt_circuit_erc
from .group import Group
from .logger import active_logger, erc_logger, stop_log_file_output
from .net import NCNet, Net
from .part import Part, PartUnit
from .pckg_info import __version__
from .pin import Pin
from .schlib import SchLib
from .scriptinfo import get_script_name, get_skidl_trace
from .skidlbaseobj import SkidlBaseObject
from .utilities import (
    expand_buses,
    export_to_all,
    flatten,
    num_to_chars,
    opened,
    reset_get_unique_name,
)


HIER_SEP = "."  # Separator for hierarchy labels.


__all__ = ["HIER_SEP"]


[docs] @export_to_all class Circuit(SkidlBaseObject): """ Class object that holds the entire netlist of parts and nets. Attributes: parts: List of all the schematic parts as Part objects. nets: List of all the schematic nets as Net objects. buses: List of all the buses as Bus objects. hierarchy: A '.'-separated concatenation of the names of nested SubCircuits at the current time it is read. level: The current level in the schematic hierarchy. context: Stack of contexts for each level in the hierarchy. """ # Set the default ERC functions for all Circuit instances. erc_list = [dflt_circuit_erc] def __init__(self, **kwargs): super().__init__() """Initialize the Circuit object.""" self.reset(init=True) # Set passed-in attributes for the circuit. for k, v in list(kwargs.items()): setattr(self, k, v) def __iadd__(self, *stuff): """Add Parts, Nets, Buses, and Interfaces to the circuit.""" return self.add_stuff(*stuff) def __isub__(self, *stuff): """Remove Parts, Nets, Buses, and Interfaces from the circuit.""" return self.rmv_stuff(*stuff) def __enter__(self): """Create a context for making this circuit the default_circuit.""" self.circuit_stack.append(default_circuit) builtins.default_circuit = self return self def __exit__(self, type, value, traceback): builtins.default_circuit = self.circuit_stack.pop()
[docs] def mini_reset(self, init=False): """Clear any circuitry but don't erase any loaded part libraries.""" self.group_name_cntr = Counter() self.name = "" self.parts = [] self.nets = [] self.netclasses = {} self.buses = [] self.interfaces = [] self.packages = deque() self.hierarchy = "top" self.level = 0 self.context = [("top",)] self.erc_assertion_list = [] self.circuit_stack = ( [] ) # Stack of previous default_circuits for context manager. self.no_files = False # Allow creation of files for netlists, ERC, libs, etc. # Internal set used to check for duplicate hierarchical names. self._hierarchical_names = {self.hierarchy} # Clear the name heap for nets and parts. 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 ) # Net for storing no-connects for parts in this circuit. 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.""" 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 add_hierarchical_name(self, name): """Record a new hierarchical name. Throw an error if it is a duplicate.""" if name in self._hierarchical_names: active_logger.raise_( ValueError, "Can't add duplicate hierarchical name {} to this circuit.".format( name ), ) self._hierarchical_names.add(name)
[docs] def rmv_hierarchical_name(self, name): """Remove an existing hierarchical name. Throw an error if non-existent.""" try: self._hierarchical_names.remove(name) except KeyError: active_logger.raise_( ValueError, "Can't remove non-existent hierarchical name {} from circuit.".format( name ), )
[docs] def activate(self, name, tag): """Save the previous hierarchical group and activate a new one.""" # Create a name for this group from the concatenated names of all # the nested contexts that were called on all the preceding levels # that led to this one. Also, add a distinct tag to the current # name to disambiguate multiple uses of the same function. This is # either specified as an argument, or an incrementing value is used. grp_hier_name = self.hierarchy + HIER_SEP + name if tag is None: tag = self.group_name_cntr[grp_hier_name] self.group_name_cntr[grp_hier_name] += 1 # Save the context from which this was called. self.context.append((default_circuit, self.hierarchy)) # Create a new hierarchical name in the activated context. self.hierarchy = self.hierarchy + HIER_SEP + name + str(tag) self.add_hierarchical_name(self.hierarchy) # Setup some globals needed in this context. builtins.default_circuit = self builtins.NC = self.NC # pylint: disable=undefined-variable
[docs] def deactivate(self): """Deactivate the current hierarchical group and return to the previous one.""" # Restore the context 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. builtins.default_circuit, self.hierarchy = self.context.pop() builtins.NC = default_circuit.NC
[docs] def add_parts(self, *parts): """Add some Part objects to the circuit.""" 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. # Store hierarchy of part. part.hierarchy = self.hierarchy # Check the part does not have a conflicting hierarchical name self.add_hierarchical_name(part.hierarchical_name) # Store part instantiation trace. part.skidl_trace = ";".join(get_skidl_trace()) self.parts.append(part) else: active_logger.raise_( ValueError, "Can't add unmovable part {} to this circuit.".format(part.ref), )
[docs] def rmv_parts(self, *parts): """Remove some Part objects from the circuit.""" for part in parts: part.disconnect() if part.is_movable(): if part.circuit == self and part in self.parts: self.rmv_hierarchical_name(part.hierarchical_name) part.circuit = None part.hierarchy = None self.parts.remove(part) else: active_logger.warning( "Removing non-existent part {} from this circuit.".format( part.ref ) ) else: active_logger.raise_( ValueError, "Can't remove part {} from this circuit.".format(part.ref), )
[docs] def add_nets(self, *nets): """Add some Net objects to the circuit. Assign a net name if necessary.""" 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 # Add the net to this circuit. net.circuit = self # Record the Circuit object the net belongs to. net.name = net.name net.hierarchy = self.hierarchy # Store hierarchy of net. self.nets.append(net) else: active_logger.raise_( ValueError, "Can't add unmovable net {} to this circuit.".format(net.name), )
[docs] def rmv_nets(self, *nets): """Remove some Net objects from the circuit.""" for net in nets: if net.is_movable(): if net.circuit == self and net in self.nets: net.circuit = None net.hierarchy = None self.nets.remove(net) else: active_logger.warning( "Removing non-existent net {} from this circuit.".format( net.name ) ) else: active_logger.raise_( ValueError, "Can't remove unmovable net {} from this circuit.".format(net.name), )
[docs] def add_buses(self, *buses): """Add some Bus objects to the circuit. Assign a bus name if necessary.""" 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 # Add the bus to this circuit. bus.circuit = self bus.name = bus.name bus.hierarchy = self.hierarchy # Store hierarchy of the bus. self.buses.append(bus) for net in bus.nets: self += net
[docs] def rmv_buses(self, *buses): """Remove some buses from the circuit.""" for bus in buses: if bus.is_movable(): if bus.circuit == self and bus in self.buses: bus.circuit = None bus.hierarchy = None self.buses.remove(bus) for net in bus.nets: self -= net else: active_logger.warning( "Removing non-existent bus {} from this circuit.".format( bus.name ) ) else: active_logger.raise_( ValueError, "Can't remove unmovable bus {} from this circuit.".format(bus.name), )
[docs] def add_packages(self, *packages): for package in packages: if package.circuit is None: if package.is_movable(): # Add the package to this circuit. self.packages.appendleft(package) package.circuit = self for obj in package.values(): try: if obj.is_movable(): obj.circuit = self except AttributeError: pass else: active_logger.raise_( ValueError, "Can't add the same package to more than one circuit.", )
[docs] def rmv_packages(self, *packages): for package in packages: if package.is_movable(): if package.circuit == self and package in self.packages: self.packages.remove(package) package.circuit = None for obj in package.values(): try: if obj.is_movable(): obj.circuit = None except AttributeError: pass else: active_logger.active_logger.warning( "Removing non-existent package {} from this circuit.".format( package.name ) ) else: active_logger.raise_( ValueError, "Can't remove unmovable package {} from this circuit.".format( package.name ), )
[docs] def add_stuff(self, *stuff): """Add Parts, Nets, Buses, and Interfaces to the circuit.""" from .package import Package 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) elif isinstance(thing, Package): self.add_packages(thing) else: active_logger.raise_( ValueError, "Can't add a {} to a Circuit object.".format(type(thing)), ) return self
[docs] def rmv_stuff(self, *stuff): """Remove Parts, Nets, Buses, and Interfaces from the circuit.""" from .package import Package 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) elif isinstance(thing, Package): self.rmv_packages(thing) else: active_logger.raise_( ValueError, "Can't remove a {} from a Circuit object.".format(type(thing)), ) return self
[docs] def get_nets(self): """Get all the distinct nets for 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 instantiate_packages(self): """Run the package executables to instantiate their circuitry.""" # Set default_circuit to this circuit and instantiate the packages. with self: while self.packages: package = self.packages.pop() # If there are still ProtoNets attached to the package at this point, # just replace them with Nets. This will allow any internal connections # inside the package to be reflected on the package I/O pins. # **THIS WILL PROBABLY FAIL IF THE INTERNAL CONNECTIONS ARE BUSES.** # DISABLE THIS FOR NOW... # for k, v in package.items(): # if isinstance(v, ProtoNet): # package[k] = Net() # Call the function to instantiate the package with its arguments. package.subcircuit(**package)
[docs] def merge_net_names(self): """Assign same name to all segments of multi-segment nets.""" 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.""" 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): """Run class-wide and local ERC functions on this circuit.""" # 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._preprocess() 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 generate_netlist(self, **kwargs): """ Return a netlist and also write it to a file/stream. Args: file_: Either a file object that can be written to, or a string containing a file name, or None. tool: The EDA tool the netlist will be generated for. do_backup: If true, create a library with all the parts in the circuit. Returns: A netlist. """ 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._preprocess() # 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) 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_: Either a file object that can be written to, or a string containing a file name, or None. tool: The EDA tool the netlist will be generated for. do_backup: If true, create a library with all the parts in the circuit. fp_libs: List of directories containing footprint libraries. Returns: None. """ 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._preprocess() # 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): """ Return netlist as an XML string and also write it to a file/stream. Args: file_: Either a file object that can be written to, or a string containing a file name, or None. tool: Backend tool such as KICAD. Returns: A string containing the netlist. """ 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._preprocess() 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 netlistsvg skin file. 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. https://eclipse.dev/elk/reference/options.html Returns: str: SVG for 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.""" # 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 file displaying the circuit schematic and return the dictionary that can be displayed by netlistsvg. Args: file_ (_type_, optional): Filename to store SVG. Defaults to None. tool (_type_, optional): Backend tool. Defaults to None. layout_options (_type_, optional): Options to control netlistsvg/ELK layout algorithm. Defaults to None. Returns: dict: JSON dictionary for input to netlistsvg. """ from . import skidl # Reset the counters to clear any warnings/errors from previous run. active_logger.error.reset() active_logger.warning.reset() self._preprocess() # 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 ) ) 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 from a Circuit. """ 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( "No footprint for {part}/{ref}.".format(part=part.name, ref=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._preprocess() 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=[], ): """ Returns a graphviz graph as graphviz object and can also write it to a file/stream. When used in ipython the graphviz object will drawn as an SVG in the output. See https://graphviz.readthedocs.io/en/stable/ and http://graphviz.org/doc/info/attrs.html Args: file_: A string containing a file name, or None. engine: See graphviz documentation rankdir: See graphviz documentation part_shape: Shape of the part nodes net_shape: Shape of the net nodes splines: Style for the edges, try 'ortho' for a schematic like feel show_values: Show values as external labels on part nodes show_anon: Show anonymous net names split_nets: splits up the plot for the given list of net names split_parts_ref: splits up the plot for all pins for the given list of part refs Returns: graphviz.Digraph """ # Reset the counters to clear any warnings/errors from previous run. active_logger.error.reset() active_logger.warning.reset() self._preprocess() 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): """ Saves parts in circuit as a SKiDL library in a file. Args: file: Either a file object that can be written to, or a string containing a file name, or None. If None, a standard library file will be used. Returns: Nothing. """ from . import skidl from skidl import SKIDL if self.no_files: return self._preprocess() 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_)
def _check_for_empty_footprints(self): """Make sure part footprints aren't empty before generating netlist/PCB.""" for part in self.parts: if getattr(part, "footprint", "") == "": import skidl skidl.empty_footprint_handler(part) def _cull_unconnected_parts(self): """Remove parts that aren't connected to anything.""" for part in self.parts: if not part.is_connected(): self -= part def _preprocess(self): """Prepare the circuit for generating a netlist, PCB, etc.""" self.instantiate_packages() # self._cull_unconnected_parts() self.merge_nets() self._check_for_empty_footprints() @property def no_files(self): """Prevent creation of output files (netlists, ERC, logs) by this Circuit object.""" return self._no_files @no_files.setter def no_files(self, stop): """Don't output any files if stop is True.""" self._no_files = stop stop_log_file_output(stop)