# -*- 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 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)