Source code for spicelib.editor.spice_subcircuit

#!/usr/bin/env python
# coding=utf-8
# -------------------------------------------------------------------------------
#
#  ███████╗██████╗ ██╗ ██████╗███████╗██╗     ██╗██████╗
#  ██╔════╝██╔══██╗██║██╔════╝██╔════╝██║     ██║██╔══██╗
#  ███████╗██████╔╝██║██║     █████╗  ██║     ██║██████╔╝
#  ╚════██║██╔═══╝ ██║██║     ██╔══╝  ██║     ██║██╔══██╗
#  ███████║██║     ██║╚██████╗███████╗███████╗██║██████╔╝
#  ╚══════╝╚═╝     ╚═╝ ╚═════╝╚══════╝╚══════╝╚═╝╚═════╝
#
# Name:        spice_subcircuit.py
# Purpose:     Representation and parsing of SPICE subcircuits
#
# Author:      Nuno Brum (nuno.brum@gmail.com)
#
# License:     refer to the LICENSE file
# -------------------------------------------------------------------------------
import io
import os
import re
from pathlib import Path
from typing import Any, Generator
import logging

from ..utils.float_unit import format_eng
from .editor_errors import *
from .spice_utils import subckt_regex, SUBCKT_CLAUSE_FIND, \
    lib_inc_regex, END_LINE_TERM, REPLACE_REGEXS, VALID_PREFIXES, UNIQUE_SIMULATION_DOT_INSTRUCTIONS
from .primitives import ValueType, Primitive, try_value
from .updates import UpdateType, UpdatePermission
from .base_subcircuit import BaseSubCircuit
from .spice_components import SpiceComponent, component_replace_regexs, _insert_section
from .spice_subcircuit_instance import SpiceCircuitInstance
from .base_editor import SUBCKT_DIVIDER, PARAM_REGEX, BaseEditor

from ..utils.detect_encoding import detect_encoding, EncodingDetectError
from ..utils.file_search import search_file_in_containers

def get_line_command(line: str | Primitive) -> str:
    """
    Retrieves the type of SPICE command in the line.
    Starts by removing the leading spaces and the evaluates if it is a comment, a directive or a component.
    """
    if isinstance(line, str):
        for i in range(len(line)):
            ch = line[i]
            if ch == ' ' or ch == '\t':
                continue
            else:
                ch = ch.upper()
                if ch in VALID_PREFIXES:  # A circuit element
                    return ch
                elif ch == '+':
                    return '+'  # This is a line continuation.
                elif ch in "#;*\n\r":  # It is a comment or a blank line
                    return "*"
                elif ch == '.':  # this is a directive
                    j = i + 1
                    while j < len(line) and (line[j] not in (' ', '\t', '\r', '\n')):
                        j += 1
                    return line[i:j].upper()
                else:
                    raise SyntaxError(f"Unrecognized command in line: \"{line}\"")
    elif isinstance(line, SpiceCircuit):
        return ".SUBCKT"
    elif isinstance(line, ControlEditor):
        return ".CONTROL"
    elif isinstance(line, IncludeFile):
        return ".INCLUDE"
    elif isinstance(line, (SpiceComponent, SpiceCircuitInstance)):
        return line.obj[0] # pyright: ignore[reportOptionalSubscript]
    elif isinstance(line, Primitive):
        return get_line_command(line.obj) # pyright: ignore[reportArgumentType]
    else:
        raise SyntaxError(f'Unrecognized command in line "{line}"')


def _is_unique_instruction(instruction):
    """
    (Private function. Not to be used directly)
    Returns true if the instruction is one of the unique instructions
    """
    cmd = get_line_command(instruction)
    return cmd in UNIQUE_SIMULATION_DOT_INSTRUCTIONS

_logger = logging.getLogger("spicelib.SpiceEditor")


def separate_lines(stream: io.StringIO) -> Generator[str, None, None]:
    """Iterator of spice lines from the stream, handling line continuations and comments.
    When a line starts with a '+', it is considered a continuation of the previous line, and the '+' is removed.
    The { ( [ and their respective closing characters will be considered as part of the line, so that parameters
    like "key={value with spaces and carriage returns}" are correctly tokenized as a single line.
    Also, if there is a return inside tokens they are removed.

    """
    current_line = ""
    braces_stack = []
    in_quotes = False
    next_char = None
    while True:
        if next_char is not None:
            char = next_char
            next_char = None
        else:
            char = stream.read(1)

        if not char:  # End of stream
            if current_line:
                yield current_line
            break

        if char in ['{', '(', '[']:
            braces_stack.append(char)
        elif char in ['}', ')', ']']:
            if braces_stack:
                closing = braces_stack.pop()
                if (char == '}' and closing != '{') or (char == ')' and closing != '(') or (char == ']' and closing != '['):
                    raise MissingExpectedClauseError(f"Mismatched braces: expected {closing} but got {char}")
            else:
                raise MissingExpectedClauseError(f"Unmatched closing brace: {char} in line: {current_line}")
        elif char == '"':
            in_quotes = not in_quotes

        elif char == '\n' or char == '\r':
            # if inside a quotes, ignore the line break and continue, otherwise treat it as a line separator
            if not (braces_stack or in_quotes):
                next_char = stream.read(1)
                if next_char == '+':
                    next_char = None  # line continuation, ignore the newline and the '+'
                elif next_char == '\n' and char=='\r' or next_char == '\r' and char=='\n':
                    # This is a Windows style line break, we need to consume the next character as well
                    # Next iteration will trigger the end of line, so we just ignore it here
                    current_line += char
                else:
                    yield current_line + char
                    current_line = ""  # reset the current line after yielding
                continue
        elif char =='*' and len(current_line)==0 and not braces_stack and not in_quotes:
            # This is a comment line, read till the end of the line
            current_line = char + stream.readline()
            if current_line:
                yield current_line
            current_line = ""  # reset the current line after yielding
            continue

        elif char == ';' and not braces_stack and not in_quotes:
            # This is a comment and the rest of the line can be consumed.
            current_line += char + stream.readline()
            yield current_line
            current_line = ""
            continue

        current_line += char


[docs] class SpiceCircuit(BaseSubCircuit): """ Represents sub-circuits within a SPICE circuit. Since sub-circuits can have sub-circuits inside them, it serves as base for the top level netlist. This hierarchical approach helps to encapsulate and protect parameters and components from edits made at a higher level. """ def __init__(self, parent: "SpiceCircuit" = None): # pyright: ignore[reportArgumentType] super().__init__(parent) self.netlist = [] def _add_lines(self, line_iter): """Internal function. Do not use. Add a list of lines to the netlist.""" self.update_permission = UpdatePermission.Initializing self._modified = False for line in line_iter: cmd = get_line_command(line) # cmd is guaranteed to be uppercased if cmd == '.SUBCKT': sub_circuit = SpiceCircuit(self) primitive = Primitive(netlist=self, obj=line) sub_circuit.netlist.append(primitive) # Advance to the next non nested .ENDS finished = sub_circuit._add_lines(line_iter) if finished: self.netlist.append(sub_circuit) else: return False elif cmd == ".CONTROL": sub_circuit = ControlEditor(netlist=self, obj=line) # Advance to the next .ENDC. There is no risk of nesting, as control sections cannot be nested. finished = sub_circuit._add_lines(line_iter) if finished: self.netlist.append(sub_circuit) else: return False elif cmd.startswith(".INC"): # This is an include statement. We need to find the file and parse it as a sub-circuit primitive = IncludeFile(netlist=self, obj=line) self.netlist.append(primitive) elif cmd == '+': assert len(self.netlist) > 0, "ERROR: The first line cannot be starting with a +" # Concatenate the line to the previous line. Make it easy to handle: just make it 1 line. (but keep spaces etc) self.netlist[-1]+=line # Append to the last line, but remove the preceding newline and the leading '+' elif len(cmd) == 1 and cmd in VALID_PREFIXES: # This is a component line if cmd == 'X': component = SpiceCircuitInstance(netlist=self, obj=line) else: component = SpiceComponent(netlist=self, obj=line) self.netlist.append(component) elif cmd == '*': # This is a comment or blank line self.netlist.append(line) else: primitive = Primitive(netlist=self, obj=line) self.netlist.append(primitive) if cmd.startswith('.END'): # True for either .END, .ENDS and .ENDC primitives self.update_permission = UpdatePermission.Inform return True # If a sub-circuit is ended correctly, returns True return False # If a sub-circuit ends abruptly, returns False
[docs] def write_lines(self, stream: io.StringIO) -> None: """Internal function. Do not use.""" # This helper function writes the contents of sub-circuit to the file stream for primitive in self.netlist: if isinstance(primitive, str): stream.write(primitive) elif isinstance(primitive, IncludeFile): if primitive.editor is None: raise RuntimeError( f"Cannot write unresolved include file: {primitive.obj!r}" ) primitive.editor.write_lines(stream) elif isinstance(primitive, (SpiceComponent, SpiceCircuit, ControlEditor)): primitive.write_lines(stream) elif isinstance(primitive, Primitive): line: str = primitive.obj # pyright: ignore[reportAssignmentType] # Writes the modified sub-circuits at the end just before the .END clause if line.upper().startswith(".END"): # write here the modified sub-circuits for sub in self.modified_subcircuits(): sub.write_lines(stream) stream.write(line.strip() + END_LINE_TERM) else: raise RuntimeError("Unknown primitive type found in netlist")
def _get_parameter_named(self, param_name) -> tuple[int, re.Match | None, list | None]: """ Internal function. Do not use. Returns a line starting with command and matching the search with the regular expression """ search_expression = re.compile(PARAM_REGEX(r"\w+"), re.IGNORECASE) param_name_upped = param_name.upper() line_no = 0 while line_no < len(self.netlist): line = self.netlist[line_no] if isinstance(line, SpiceCircuit): # If it is a sub-circuit it will simply ignore it. line_no += 1 continue elif isinstance(line, ControlEditor): # same for control editor line_no += 1 continue elif isinstance(line, IncludeFile): # same for include files if line.editor and isinstance(line.editor, SpiceCircuit): sub_circuit = line.editor.get_subcircuit_named(param_name_upped) if sub_circuit: sub_ckt_line_no, match, netlist = sub_circuit._get_parameter_named(param_name_upped) if match: return sub_ckt_line_no, match, netlist elif isinstance(line, Primitive): line: str = line.obj cmd = get_line_command(line) # pyright: ignore[reportArgumentType] if cmd == '.PARAM': matches = search_expression.finditer(line) for match in matches: if match.group("name").upper() == param_name_upped: return line_no, match, self.netlist line_no += 1 return -1, None, None # If it fails, it returns an invalid line number and No match
[docs] def get_all_parameter_names(self) -> list[str]: # docstring inherited from BaseEditor param_names = [] search_expression = re.compile(PARAM_REGEX(r"\w+"), re.IGNORECASE) for line in self.netlist: if isinstance(line, Primitive): line: str = line.obj # pyright: ignore[reportAssignmentType] cmd = get_line_command(line) if cmd == '.PARAM': matches = search_expression.finditer(line) for match in matches: param_name = match.group('name') param_names.append(param_name.upper()) return sorted(param_names)
[docs] def get_subcircuit_names(self) -> list[str]: """ Returns a list of the names of the sub-circuits in the netlist. :return: list of sub-circuit names :rtype: list[str] """ subckt_names = [] for line in self.netlist: if isinstance(line, SpiceCircuit): subckt_names.append(line.name()) return subckt_names
[docs] def get_subcircuit_named(self, name: str) -> BaseSubCircuit | None: """ Returns the sub-circuit object with the given name. :param name: name of the subcircuit :type name: str :return: _description_ :rtype: _type_ """ for line in self.netlist: if isinstance(line, SpiceCircuit): if line.name() == name: return line elif isinstance(line, IncludeFile): # Searches inside the Include File if line.editor: sub_ckt = line.editor.get_subcircuit_named(name) if sub_ckt: return sub_ckt if self.parent is not None: return self.parent.get_subcircuit_named(name) return self.find_subckt_in_included_libs(name) # If it is not found in the current circuit, it may be in the included libraries
[docs] def get_subcircuit(self, instance_name: str) -> 'SpiceCircuit': """ Returns an object representing a Subcircuit. This object can manipulate elements such as the SpiceEditor does. :param instance_name: Reference of the subcircuit :type instance_name: str :returns: SpiceCircuit instance :rtype: SpiceCircuit :raises UnrecognizedSyntaxError: when an spice command is not recognized by spicelib :raises ComponentNotFoundError: When the reference was not found """ sub_inst = self.get_component(instance_name) assert isinstance(sub_inst, SpiceCircuitInstance) return sub_inst.subcircuit
[docs] def reset_netlist(self, **kwargs) -> bool: """ Reverts all changes done to the netlist. :returns: None """ self.netlist.clear() return True
[docs] def clone(self, new_parent, **kwargs) -> 'SpiceCircuit': """ Creates a new copy of the SpiceCircuit. Changes done at the new copy do not affect the original. :key new_name: The new name to be given to the circuit :key type new_name: str :return: The new replica of the SpiceCircuit object :rtype: SpiceCircuit """ clone = SpiceCircuit(new_parent) clone.netlist.append( "***** SpiceEditor Manipulated this sub-circuit ****" + END_LINE_TERM) for primitive in self.netlist: if isinstance(primitive, SpiceCircuit): clone.netlist.append(primitive.clone(clone)) elif isinstance(primitive, SpiceComponent): clone.netlist.append(primitive.clone(clone)) elif isinstance(primitive, Primitive): clone.netlist.append(Primitive(netlist=clone, obj=primitive.obj)) else: clone.netlist.append(primitive) clone.netlist.append("***** ENDS SpiceEditor ****" + END_LINE_TERM) new_name = kwargs.get('new_name', None) if new_name is not None and isinstance(new_name, str): clone.setname(new_name) clone.update_permission = UpdatePermission.Inform return clone
[docs] def name(self) -> str: """ Returns the name of the Sub-Circuit. :rtype: str """ if len(self.netlist): for line in self.netlist: if isinstance(line, Primitive): line: str = line.obj # pyright: ignore[reportAssignmentType] m = subckt_regex.search(line) if m: return m.group('name') if self.editor and self.editor.circuit_file: return self.editor.circuit_file.name return "Netlist"
[docs] def setname(self, new_name: str): """ Renames the sub-circuit to a new name. No check is done to the new name. It is up to the user to make sure that the new name is valid. :param new_name: The new Name. :type new_name: str :return: Nothing """ if len(self.netlist): lines = len(self.netlist) line_no = 0 while line_no < lines: line = self.netlist[line_no] if isinstance(line, Primitive): line: str = line.obj # pyright: ignore[reportAssignmentType] m = subckt_regex.search(line) if m: # Replacing the name in the SUBCKT Clause start = m.start('name') end = m.end('name') # print(f"Replacing '{line[start:end]}' with '{new_name}'") self.netlist[line_no] = _insert_section(line, start, end, new_name) + END_LINE_TERM break line_no += 1 else: raise MissingExpectedClauseError("Unable to find .SUBCKT clause in subcircuit") # This second loop finds the .ENDS clause while line_no < lines: line = self.netlist[line_no] if isinstance(line, Primitive): line_obj: str = line.obj # pyright: ignore[reportAssignmentType] if get_line_command(line_obj) == '.ENDS': line._obj = '.ENDS ' + new_name + END_LINE_TERM break line_no += 1 else: raise MissingExpectedClauseError("Unable to find .SUBCKT clause in subcircuit") else: # Avoiding exception by creating an empty sub-circuit self.netlist.append("* SpiceEditor Created this sub-circuit") self.netlist.append(Primitive(netlist=self, obj='.SUBCKT %s%s' % (new_name, END_LINE_TERM))) self.netlist.append(Primitive(netlist=self, obj='.ENDS %s%s' % (new_name, END_LINE_TERM)))
[docs] def get_component(self, reference: str) -> 'SpiceComponent | SpiceCircuitInstance | SpiceCircuit': """ Returns an object representing the given reference in the schematic file. :param reference: Reference of the component :return: The SpiceComponent object or a SpiceSubcircuit in case of hierarchical design :raises: ComponentNotFoundError - In case the component is not found :raises: UnrecognizedSyntaxError when the line doesn't match the expected REGEX. :raises: NotImplementedError if there isn't an associated regular expression for the component prefix. """ ref_upped = reference.upper() if SUBCKT_DIVIDER in ref_upped: if ref_upped[0] != 'X': # It needs be contained in a sub-circuit declaration raise ComponentNotFoundError("Only subcircuits can have components inside.") else: # In this case the sub-circuit needs to be copied so that is copy is modified. # A copy is created for each instance of a sub-circuit. subckt_ref, sub_ref = ref_upped.split(SUBCKT_DIVIDER, 1) subcircuit_instance : SpiceCircuitInstance = self.get_component(subckt_ref) # pyright: ignore[reportAssignmentType] component_type = subcircuit_instance.get_component(sub_ref) return component_type # pyright: ignore[reportReturnType] # Component is converted to SpiceComponent else: for component in self.netlist: # This test needs to be done before SpiceComponent because this is a Subclass of SpiceComponent if isinstance(component, SpiceCircuit): name = component.name() if name.upper() == ref_upped: return component elif isinstance(component, SpiceComponent): if component.reference.upper() == ref_upped: # if isinstance(component, SpiceCircuitInstance): # # Need to check whether a shadow exists # pass return component error_msg = "line starting with '%s' not found in netlist" % reference _logger.error(error_msg) raise ComponentNotFoundError(error_msg)
def __getitem__(self, item) -> SpiceComponent: component = super().__getitem__(item) return component# pyright: ignore[reportReturnType] # Component is converted to SpiceComponent def __delitem__(self, key): """ This method allows the user to delete a component using the syntax: del circuit['R1'] """ self.remove_component(key) def __contains__(self, key): """ This method allows the user to check if a component is in the circuit using the syntax: 'R1' in circuit """ try: self.get_component(key) return True except ComponentNotFoundError: return False def __iter__(self): """ This method allows the user to iterate over the components in the circuit using the syntax: for component in circuit: print(component) """ for line_no, line in enumerate(self.netlist): if isinstance(line, SpiceCircuit): yield from line elif isinstance(line, ControlEditor): continue # no components here, just control commands elif isinstance(line, IncludeFile): yield from line else: cmd = get_line_command(line) if cmd in VALID_PREFIXES: yield SpiceComponent(self, line_no)
[docs] def get_component_attribute(self, reference: str, attribute: str) -> str | None: """ Returns the attribute of a component retrieved from the netlist. :param reference: Reference of the component :type reference: str :param attribute: Name of the attribute to be retrieved :type attribute: str :return: Value of the attribute :rtype: str :raises: ComponentNotFoundError - In case the component is not found :raises: UnrecognizedSyntaxError when the line doesn't match the expected REGEX. :raises: NotImplementedError if there isn't an associated regular expression for the component prefix. """ component = self.get_component(reference) if isinstance(component, SpiceCircuit): raise ValueError(f"Component '{reference}' is a sub-circuit. Use get_subcircuit() instead.") else: return component.attributes.get(attribute, None)
[docs] def get_component_parameters(self, reference: str) -> dict: # docstring inherited from BaseEditor component = self.get_component(reference) if isinstance(component, SpiceCircuit): raise ValueError(f"Component '{reference}' is a sub-circuit. Use get_subcircuit() instead.") answer = {} answer.update(component.params) # Now check if there is a value parameter # NOTE: This is a legacy behavior that may be removed in future versions. if hasattr(component, 'value'): answer['Value'] = component.value_str return answer
[docs] def set_component_parameters(self, reference: str, **kwargs) -> None: # docstring inherited from BaseEditor if self.is_read_only(): raise ValueError("Editor is read-only") self.get_component(reference).set_parameters(**kwargs)
[docs] def get_parameter(self, param: str) -> str: """ Returns the value of a parameter retrieved from the netlist. :param param: Name of the parameter to be retrieved :type param: str :return: Value of the parameter being sought :rtype: str :raises: ParameterNotFoundError - In case the component is not found """ _, match, _ = self._get_parameter_named(param) if match: return try_value(match.group('value')) else: raise ParameterNotFoundError(param, f"circuit {self.name()}")
[docs] def set_parameter(self, param: str, value: ValueType) -> None: """Sets the value of a parameter in the netlist. If the parameter is not found, it is added to the netlist. Usage: :: runner.set_parameter("TEMP", 80) This adds onto the netlist the following line: :: .PARAM TEMP=80 This is an alternative to the set_parameters which is more pythonic in its usage and allows setting more than one parameter at once. :param param: Spice Parameter name to be added or updated. :type param: str :param value: Parameter Value to be set. :type value: str, int or float :return: Nothing """ permission = self.begin_update() if permission == UpdatePermission.Deny: raise ValueError("Editor is read-only") if isinstance(value, (int, float)): value_str = format_eng(value) elif isinstance(value, complex): value_str = format_eng(value.real) + "+" + format_eng(value.imag) + "j" else: value_str = value param_line, match, netlist = self._get_parameter_named(param) if match: start, stop = match.span('value') if isinstance(netlist[param_line], Primitive): netlist[param_line]._obj = _insert_section(netlist[param_line].obj, start, stop, f"{value_str}") + END_LINE_TERM else: netlist[param_line] = _insert_section(netlist[param_line], start, stop, f"{value_str}") + END_LINE_TERM if permission == UpdatePermission.Inform: self.end_update(param, value_str, UpdateType.UpdateParameter) else: # Was not found # the last two lines are typically (.backano and .end) insert_line = len(self.netlist) - 2 term = Primitive(netlist=self, obj=f'.PARAM {param}={value_str} ; Batch instruction' + END_LINE_TERM) self.netlist.insert(insert_line, term) if permission == UpdatePermission.Inform: self.end_update(param, value, UpdateType.AddParameter)
[docs] def set_component_value(self, reference: str, value: ValueType) -> None: """ Changes the value of a component, such as a Resistor, Capacitor or Inductor. For components inside sub-circuits, use the sub-circuit designator prefix with ':' as separator (Example X1:R1) Usage: :: runner.set_component_value('R1', '3.3k') runner.set_component_value('X1:C1', '10u') :param reference: Reference of the circuit element to be updated. :type reference: str :param value: value to be set on the given circuit element. Float and integer values will be automatically formatted as per the engineering notations 'k' for kilo, 'm', for mili and so on. :type value: str, int or float :raises: ComponentNotFoundError - In case the component is not found ValueError - In case the value doesn't correspond to the expected format NotImplementedError - In case the circuit element is defined in a format which is not supported by this version. If this is the case, use GitHub to start a ticket. https://github.com/nunobrum/spicelib """ component = self.get_component(reference) assert isinstance(component, SpiceComponent), f"Component '{reference}' is not a SpiceComponent. Use set_element_model() instead." component.set_value(value)
[docs] def set_element_model(self, reference: str, model: str) -> None: """Changes the value of a circuit element, such as a diode model or a voltage supply. Usage: :: runner.set_element_model('D1', '1N4148') runner.set_element_model('V1' "SINE(0 1 3k 0 0 0)") :param reference: Reference of the circuit element to be updated. :type reference: str :param model: model name of the device to be updated :type model: str :raises: ComponentNotFoundError - In case the component is not found ValueError - In case the model format contains irregular characters NotImplementedError - In case the circuit element is defined in a format which is not supported by this version. If this is the case, use GitHub to start a ticket. https://github.com/nunobrum/spicelib """ permission = self.begin_update() if permission == UpdatePermission.Deny: raise ValueError("Editor is read-only") component = self.get_component(reference) assert isinstance(component, SpiceComponent), f"Component '{reference}' is not a SpiceComponent. Use set_element_model() instead." component.set_value(model) if permission == UpdatePermission.Inform: self.end_update(reference, model, UpdateType.UpdateComponentValue)
[docs] def get_component_value(self, reference: str) -> str | None: """ Returns the value of a component retrieved from the netlist. :param reference: Reference of the circuit element to get the value. :type reference: str :return: value of the circuit element . :raises: ComponentNotFoundError - In case the component is not found NotImplementedError - for not supported operations """ component = self.get_component(reference) assert isinstance(component, SpiceComponent), f"Component '{reference}' is not a SpiceComponent. Use get_element_model() instead." return component.value_str
[docs] def get_component_nodes(self, reference: str) -> list[str]: """ Returns the nodes to which the component is attached to. :param reference: Reference of the circuit element to get the nodes. :type reference: str :return: List of nodes :rtype: list[str] """ component = self.get_component(reference) assert isinstance(component, SpiceComponent), f"Component '{reference}' is not a SpiceComponent. Use get_element_model() instead." nodes = component.port_list() return nodes
[docs] def get_components(self, prefixes='*') -> list: """ Returns a list of components that match the list of prefixes indicated on the parameter prefixes. In case prefixes is left empty, it returns all the ones that are defined by the REPLACE_REGEXES. The list will contain the designators of all components found. :param prefixes: Type of prefixes to search for. Examples: 'C' for capacitors; 'R' for Resistors; etc... See prefixes in SPICE documentation for more details. The default prefix is '*' which is a special case that returns all components. :type prefixes: str :return: A list of components matching the prefixes demanded. """ answer = [] if prefixes == '*': prefixes = ''.join(VALID_PREFIXES) for component in self.netlist: if isinstance(component, SpiceComponent): # Only gets components from the main netlist, reference = component.reference try: if reference[0] in prefixes: answer.append(reference) # Appends only the designators except IndexError or TypeError: pass return answer
[docs] def add_component(self, component: SpiceComponent, **kwargs) -> None: """ Adds a component to the netlist. The component is added to the end of the netlist, just before the .END statement. If the component already exists, it will be replaced by the new one. :param component: The component to be added to the netlist :type component: Component :param kwargs: The following keyword arguments are supported: * **insert_before** (str) - The reference of the component before which the new component should be inserted. * **insert_after** (str) - The reference of the component after which the new component should be inserted. :return: Nothing """ permission = self.begin_update() if permission == UpdatePermission.Deny: raise ValueError("Editor is read-only") if 'insert_before' in kwargs: comp = self.get_component(kwargs['insert_before']) line_no = self.netlist.index(comp) elif 'insert_after' in kwargs: comp = self.get_component(kwargs['insert_after']) line_no = self.netlist.index(comp) + 1 else: # Insert before backanno instruction try: line_no = self.netlist.index( '.backanno\n') # TODO: Improve this. END of line termination could be differnt except ValueError: line_no = len(self.netlist) - 2 self.netlist.insert(line_no, component) if permission == UpdatePermission.Inform: self.end_update(component.reference, component.value_str, UpdateType.AddComponent)
[docs] def remove_component(self, designator: str) -> None: """ Removes a component from the design. Current implementation only allows removal of a component from the main netlist, not from a sub-circuit. :param designator: Component reference in the design. Ex: V1, C1, R1, etc... :type designator: str :return: Nothing :raises: ComponentNotFoundError - When the component doesn't exist on the netlist. """ permission = self.begin_update() if permission == UpdatePermission.Deny: raise ValueError("Editor is read-only") line = self.netlist.index(self.get_component(designator)) del self.netlist[line] if permission == UpdatePermission.Inform: self.end_update(designator, "delete", UpdateType.DeleteComponent)
[docs] def get_all_nodes(self) -> list[str]: """ Retrieves all nodes existing on a Netlist. :returns: Circuit Nodes :rtype: list[str] """ circuit_nodes = [] for line in self.netlist: if isinstance(line, (SpiceComponent, SpiceCircuitInstance)): prefix = get_line_command(line) if prefix in component_replace_regexs: regex = component_replace_regexs[prefix] match = regex.match(line.obj) # pyright: ignore[reportAssignmentType] if match: nodes = match.group('nodes').split() # This separates by all space characters including \t for node in nodes: if node not in circuit_nodes: circuit_nodes.append(node) return circuit_nodes
[docs] def save_netlist(self, run_netlist_file: str | Path | io.StringIO) -> None: # docstring is in the parent class pass
[docs] def class_for_instruction(self, instruction, cmd=""): if cmd == "": cmd = get_line_command(instruction) if cmd == ".CONTROL": # If it is a control instruction, then it should be added as a ControlEditor c = ControlEditor(self, instruction) return c elif cmd.startswith(".INC"): # An include file return IncludeFile(netlist=self, obj=instruction) elif cmd in VALID_PREFIXES: # If it is a component, then it should be added as a SpiceComponent return SpiceComponent(netlist=self, obj=instruction) elif cmd.startswith('.'): # Otherwise, it is a Primitive return Primitive(netlist=self, obj=instruction) else: return instruction
[docs] def add_instruction(self, instruction: str) -> None: # docstring in parent class cmd = get_line_command(instruction) if _is_unique_instruction(cmd): raise RuntimeError(f"Simulation directives like \"{cmd}\" can't be set on sub-circuits") if cmd == '.PARAM': raise RuntimeError('The .PARAM instruction should be added using the "set_parameter" method') # check whether the instruction is already there (dummy proofing) for line in self.netlist: if isinstance(line, Primitive): line_obj: str = line.obj # pyright: ignore[reportAssignmentType] if line_obj.strip() == instruction.strip(): _logger.warning( f'Instruction "{instruction.strip()}" is already present in the netlist. Ignoring addition.') return # TODO: if adding a .MODEL or .SUBCKT it should verify if it already exists and update it. if not instruction.endswith(END_LINE_TERM): instruction += END_LINE_TERM # Insert at the end primitive = self.class_for_instruction(instruction, cmd) line = len(self.netlist) - 1 # Just before the ENDS self.netlist.insert(line, primitive)
[docs] def remove_instruction(self, instruction) -> bool: # docstring is in the parent class # TODO: Make it more intelligent so it recognizes .models, .param and .subckt if self.begin_update() == UpdatePermission.Deny: _logger.warning("Permission denied") return False i = 0 for line in self.netlist: if isinstance(line, Primitive): line_obj: str = line.obj # pyright: ignore[reportAssignmentType] if line_obj.strip() == instruction.strip(): del self.netlist[i] logtxt = instruction.strip().replace("\r", "\\r").replace("\n", "\\n") _logger.info(f'Instruction "{logtxt}" removed') self.end_update('INSTRUCTION', logtxt, UpdateType.DeleteInstruction) return True # All other cases are ignored i += 1 _logger.error(f'Instruction "{instruction}" not found.') return False
[docs] def remove_Xinstruction(self, search_pattern: str) -> bool: # docstring is in the parent class if self.begin_update() == UpdatePermission.Deny: _logger.warning("Permission denied") return False regex = re.compile(search_pattern, re.IGNORECASE) i = 0 instr_removed = False while i < len(self.netlist): line = self.netlist[i] if isinstance(line, Primitive): line = line.obj if isinstance(line, str) and (match := regex.match(line)): del self.netlist[i] instr_removed = True self.end_update('INSTRUCTION', match.string.strip(), UpdateType.DeleteInstruction) _logger.info(f'Instruction "{line}" removed') else: i += 1 if instr_removed: return True else: _logger.error(f'No instruction matching pattern "{search_pattern}" was found') return False
[docs] def is_read_only(self) -> bool: """Check if the component can be edited. This is useful when the editor is used on non modifiable files. :return: True if the component is read-only, False otherwise :rtype: bool """ editor = self.editor return editor is None or editor.update_permission == UpdatePermission.Deny
@staticmethod def find_subckt_in_lib(library: str, subckt_name: str) -> 'SpiceCircuit | None': """ Finds a sub-circuit in a library. The search is case-insensitive. :param library: path to the library to search :type library: str :param subckt_name: sub-circuit to search for :type subckt_name: str :return: Returns a SpiceCircuit instance with the sub-circuit found or None if not found :rtype: SpiceCircuit :meta private: """ # 0. Setup things reg_subckt = re.compile(SUBCKT_CLAUSE_FIND + subckt_name, re.IGNORECASE) # 1. Find Encoding try: encoding = detect_encoding(library, r"[\* a-zA-Z]") except EncodingDetectError: return None # 2. scan the file with open(library, encoding=encoding) as lib: line_iterator = separate_lines(lib) # pyright: ignore[reportArgumentType] for line in line_iterator: search = reg_subckt.match(line) if search: sub_circuit = SpiceCircuit() sub_circuit.netlist.append(Primitive(netlist=sub_circuit, obj=line)) # Advance to the next non nested .ENDS finished = sub_circuit._add_lines(line_iterator) if finished: # if this is from a lib, don't allow modifications sub_circuit._readonly = True return sub_circuit # 3. Return an instance of SpiceCircuit return None def find_library(self, library_name: str) -> str | None: """Find the library in the list of libraries :param library_name: library to search for :type library_name: str :return: Returns the path to the library or None if not found :rtype: str :meta private: """ containers = [] if self.editor and self.editor.circuit_file: containers.append(os.path.split(self.editor.circuit_file)[0]) # The directory where the file is located containers.append(os.path.curdir) # The current script directory, if self.editor: containers.extend(self.editor.simulator_lib_paths) # The simulator's library paths containers.extend(self.editor.custom_lib_paths) # The custom library paths return search_file_in_containers(library_name, *containers) def find_subckt_in_included_libs(self, subcircuit_name: str) -> 'SpiceCircuit | None': """Find the subcircuit in the list of libraries :param subcircuit_name: sub-circuit to search for :type subcircuit_name: str :return: Returns a SpiceCircuit instance with the sub-circuit found or None if not found :rtype: SpiceCircuit :meta private: """ for line in self.netlist: if isinstance(line, SpiceCircuit): # If it is a sub-circuit it will simply ignore it. continue elif isinstance(line, ControlEditor): # same for control editor continue elif isinstance(line, Primitive): line: str = line.obj # pyright: ignore[reportAssignmentType] m = lib_inc_regex.match(line) if m: # If it is a library include lib = m.group('filename') lib_filename = self.find_library(lib) if lib_filename: sub_circuit = self.find_subckt_in_lib(lib_filename, subcircuit_name) if sub_circuit: # Success we can go out # by the way, this circuit will have been marked as readonly return sub_circuit if self.parent is not None: # try searching on parent netlists parent: SpiceCircuit = self.parent # pyright: ignore[reportAssignmentType] return parent.find_subckt_in_included_libs(subcircuit_name) else: return None
[docs] def modified_subcircuits(self) -> list['SpiceCircuit']: """ Returns a list of all sub-circuits that have been modified. :return: List of modified sub-circuits :rtype: list[SpiceCircuit] """ modified = [] for subckt in self.netlist: if isinstance(subckt, SpiceCircuitInstance) and subckt.was_modified: modified.append(subckt.shadow_subcircuit) return modified
class ControlEditor(Primitive): """ Provides interfaces to manipulate SPICE `.control` instructions. """ def _add_lines(self, line_iter): """Internal function. Do not use. Add a list of lines to the section. No parsing, just loop until a .ENDC is found.""" self._obj = self._obj.rstrip() + END_LINE_TERM for line in line_iter: self._obj += line.rstrip() + END_LINE_TERM if line.strip().upper().startswith(".ENDC"): return True return False # If a file ends abruptly, returns False def write_lines(self, f: io.StringIO): """Internal function. Do not use.""" # This helper function writes the contents of the section to the file f f.write(self._obj) @property def content(self) -> str: """The content as a string :getter: Returns the value as a string """ return self._obj @content.setter def content(self, value: str): """Sets the content of the ControlEditor to the given value. :param value: The new content to be set :type value: str """ self._obj = value.strip() + END_LINE_TERM class IncludeFile(Primitive): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) m = lib_inc_regex.match(self._obj) editor = None if m: lib_name = m.group('filename') include_file = self._netlist.find_library(lib_name) if include_file: from .spice_editor import SpiceEditor try: encoding = getattr(self._netlist, 'encoding', None) if encoding is None: parent_editor = getattr(self._netlist, 'editor', None) encoding = getattr(parent_editor, 'encoding', None) editor = SpiceEditor(include_file, encoding=encoding, include_file=True) except Exception as e: _logger.error(f"Error loading library '{lib_name}': {e}") else: _logger.error(f"Could not find library '{lib_name}'") else: _logger.error(f"Invalid .INCLUDE statement: {self._obj}") self._editor = editor @property def editor(self) -> 'SpiceEditor | None': return self._editor def __iter__(self): editor = self.editor if editor is None: return iter(()) return iter(editor)