Source code for spicelib.editor.asc_editor

#!/usr/bin/env python
# -------------------------------------------------------------------------------
#
#  ███████╗██████╗ ██╗ ██████╗███████╗██╗     ██╗██████╗
#  ██╔════╝██╔══██╗██║██╔════╝██╔════╝██║     ██║██╔══██╗
#  ███████╗██████╔╝██║██║     █████╗  ██║     ██║██████╔╝
#  ╚════██║██╔═══╝ ██║██║     ██╔══╝  ██║     ██║██╔══██╗
#  ███████║██║     ██║╚██████╗███████╗███████╗██║██████╔╝
#  ╚══════╝╚═╝     ╚═╝ ╚═════╝╚══════╝╚══════╝╚═╝╚═════╝
#
# Name:        asc_editor.py
# Purpose:     Class made to update directly the LTspice ASC files
#
# Author:      Nuno Brum (nuno.brum@gmail.com)
#
# Licence:     refer to the LICENSE file
# -------------------------------------------------------------------------------
import os.path
import sys
from pathlib import Path
import io
from ..utils.detect_encoding import detect_encoding, EncodingDetectError
import re
import logging

from .ltspice_utils import TEXT_REGEX, TEXT_REGEX_X, TEXT_REGEX_Y, TEXT_REGEX_ALIGN, TEXT_REGEX_SIZE, TEXT_REGEX_TYPE, \
    TEXT_REGEX_TEXT, END_LINE_TERM, ASC_ROTATION_DICT, ASC_INV_ROTATION_DICT, asc_text_align_set, asc_text_align_get
from .spice_editor import SpiceEditor, SpiceCircuit
from ..simulators.ltspice_simulator import LTspice
from ..utils.file_search import search_file_in_containers
from .base_editor import format_eng, ComponentNotFoundError, ParameterNotFoundError, PARAM_REGEX, \
    UNIQUE_SIMULATION_DOT_INSTRUCTIONS, ValueType
from .base_schematic import (BaseSchematic, Point, Line, Shape, Text, SchematicComponent, ERotation, TextTypeEnum, Port)
from .asy_reader import AsyReader

from ..log.logfile_data import try_convert_value

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


LTSPICE_PARAMETERS = ("Value", "Value2", "SpiceModel", "SpiceLine", "SpiceLine2")
LTSPICE_PARAMETERS_REDUCED = ("SpiceLine", "SpiceLine2")
LTSPICE_ATTRIBUTES = ("InstName", "Def_Sub")


class AscEditor(BaseSchematic):
    """Class made to update directly the LTspice ASC files"""
    symbol_cache = {}  # This is a class variable, so it can be shared between all instances.
    """:meta private:"""
    
    simulator_lib_paths: list[str] = LTspice.get_default_library_paths()
    """ This is initialised with typical locations found for LTspice.
    You can (and should, if you use wine), call `prepare_for_simulator()` once you've set the executable paths.
    This is a class variable, so it will be shared between all instances.
    
    :meta hide-value:
    """
    
    def __init__(self, asc_file: str | Path, encoding='autodetect'):
        super().__init__()
        self.version = 4
        self.sheet = "1 0 0"  # Three values are present on the SHEET clause
        self.asc_file_path = Path(asc_file)
        if not self.asc_file_path.exists():
            raise FileNotFoundError(f"File {asc_file} not found")
        # determine encoding
        if encoding == 'autodetect':
            try:
                self.encoding = detect_encoding(self.asc_file_path, r'^VERSION ', re_flags=re.IGNORECASE)  # Normally the file will start with 'VERSION '
            except EncodingDetectError as err:
                raise err
        else:
            self.encoding = encoding  
        # read the file into memory
        self.reset_netlist()

    @property
    def circuit_file(self) -> Path:
        return self.asc_file_path

    def save_netlist(self, run_netlist_file: str | Path | io.StringIO) -> None:
        """
        Saves the current state of the netlist to a .asc file. 
        For writing to a .net or .cir file, use the `LTspice.create_netlist()` method instead.

        :param run_netlist_file: File name of the netlist file.
        :returns: Nothing
        """
        if isinstance(run_netlist_file, io.StringIO):
            asc = run_netlist_file
        else:
            if isinstance(run_netlist_file, str):
                run_netlist_file = Path(run_netlist_file)
            if run_netlist_file.suffix in ('.net', '.cir'):
                raise ValueError("Use the `LTspice.create_netlist()` method instead")
            if run_netlist_file.suffix != '.asc':
                run_netlist_file = run_netlist_file.with_suffix(".asc")
            asc = open(run_netlist_file, 'w', encoding=self.encoding)

        try:
            _logger.info(f"Writing ASC file {run_netlist_file}")

            asc.write(f"Version {self.version}" + END_LINE_TERM)
            asc.write(f"SHEET {self.sheet}" + END_LINE_TERM)
            for wire in self.wires:
                asc.write(f"WIRE {wire.V1.X} {wire.V1.Y} {wire.V2.X} {wire.V2.Y}" + END_LINE_TERM)
            for flag in self.labels:
                asc.write(f"FLAG {flag.coord.X} {flag.coord.Y} {flag.text}" + END_LINE_TERM)
            for component in self.components.values():
                symbol = component.symbol
                posX = component.position.X
                posY = component.position.Y
                rotation = ASC_INV_ROTATION_DICT[component.rotation]
                asc.write(f"SYMBOL {symbol} {posX} {posY} {rotation}" + END_LINE_TERM)
                for attr, value in component.attributes.items():
                    if attr.startswith('_WINDOW') and isinstance(value, Text):
                        num_ref = attr[len("_WINDOW_"):]
                        posX = value.coord.X
                        posY = value.coord.Y
                        alignment = asc_text_align_get(value)
                        size = value.size
                        asc.write(f"WINDOW {num_ref} {posX} {posY} {alignment} {size}" + END_LINE_TERM)
                asc.write(f"SYMATTR InstName {component.reference}" + END_LINE_TERM)
                if component.reference.startswith('X') and "_SUBCKT" in component.attributes:
                    # writing the sub-circuit if it was updated
                    sub_circuit: AscEditor = component.attributes['_SUBCKT']
                    if sub_circuit is not None and sub_circuit.updated:
                        sub_circuit.save_netlist(sub_circuit.asc_file_path)
                for attr, value in component.attributes.items():
                    if not attr.startswith('_'):  # All these are not exported since they are only used internally
                        asc.write(f"SYMATTR {attr} {value}" + END_LINE_TERM)
            for directive in self.directives:
                posX = directive.coord.X
                posY = directive.coord.Y
                alignment = asc_text_align_get(directive)
                size = directive.size
                if directive.type == TextTypeEnum.DIRECTIVE:
                    directive_type = '!'
                else:
                    directive_type = ';'  # Otherwise assume it is a comment
                asc.write(f"TEXT {posX} {posY} {alignment} {size} {directive_type}{directive.text}" + END_LINE_TERM)
            for line in self.lines:
                line_style = f' {line.style.pattern}' if line.style.pattern != "" else ""
                asc.write(f"LINE Normal {line.V1.X} {line.V1.Y} {line.V2.X} {line.V2.Y}{line_style}" + END_LINE_TERM)
            for shape in self.shapes:
                line_style = f' {shape.line_style.pattern}' if shape.line_style.pattern != "" else ""
                points = " ".join([f"{point.X} {point.Y}" for point in shape.points])
                asc.write(f"{shape.name} Normal {points}{line_style}" + END_LINE_TERM)

        finally:
            if not isinstance(run_netlist_file, io.StringIO):
                asc.close()

[docs] def reset_netlist(self, create_blank: bool = False) -> None: super().reset_netlist() with open(self.asc_file_path, encoding=self.encoding) as asc_file: _logger.info(f"Parsing ASC file {self.asc_file_path}") component = None for line in asc_file: if line.startswith("SYMBOL"): if component is not None: # store previous component assert component.reference is not None, "Component InstName was not given" self.components[component.reference] = component component = SchematicComponent(self, line) tag, *symbol, posX, posY, rotation = line.split() component.symbol = symbol[0] if len(symbol) == 1 else ' '.join(symbol) component.position.X = int(posX) component.position.Y = int(posY) if rotation in ASC_ROTATION_DICT: component.rotation = ASC_ROTATION_DICT[rotation] else: raise ValueError(f"Invalid Rotation value: {rotation}") elif line.startswith("WINDOW"): assert component is not None, "Syntax Error: WINDOW clause without SYMBOL" tag, num_ref, posX, posY, alignment, size = line.split() component.append(line) coord = Point(int(posX), int(posY)) text = Text(coord=coord, text=num_ref, size=size, type=TextTypeEnum.ATTRIBUTE) text = asc_text_align_set(text, alignment) component.attributes['_WINDOW ' + num_ref] = text elif line.startswith("SYMATTR"): assert component is not None, "Syntax Error: SYMATTR clause without SYMBOL" component.append(line) tag, ref, text = line.split(maxsplit=2) text = text.strip() # Gets rid of the \n terminator if ref == "InstName": component.reference = text symbol = self._get_symbol(component.symbol) if component.reference.startswith('X') or symbol.is_subcircuit(): # This is a subcircuit # then create the attribute "SUBCKT" component.attributes['_SUBCKT'] = self._get_subcircuit(symbol) else: # make sure prefix is uppercase, as this is used in a lot of places if ref.upper() == "PREFIX": text = text.upper() component.attributes[ref] = text elif line.startswith("TEXT"): match = TEXT_REGEX.match(line) if match: text = match.group(TEXT_REGEX_TEXT) X = int(match.group(TEXT_REGEX_X)) Y = int(match.group(TEXT_REGEX_Y)) coord = Point(X, Y) size = int(match.group(TEXT_REGEX_SIZE)) if match.group(TEXT_REGEX_TYPE) == "!": ttype = TextTypeEnum.DIRECTIVE else: ttype = TextTypeEnum.COMMENT alignment = match.group(TEXT_REGEX_ALIGN) text = Text(coord=coord, text=text.strip(), size=size, type=ttype) text = asc_text_align_set(text, alignment) self.directives.append(text) elif line.startswith("WIRE"): tag, x1, y1, x2, y2 = line.split() v1 = Point(int(x1), int(y1)) v2 = Point(int(x2), int(y2)) wire = Line(v1, v2) self.wires.append(wire) elif line.startswith("FLAG"): tag, posX, posY, text = line.split(maxsplit=4) coord = Point(int(posX), int(posY)) flag = Text(coord=coord, text=text, type=TextTypeEnum.LABEL) self.labels.append(flag) elif line.startswith("Version"): tag, version = line.split() assert version in ["4", "4.0", "4.1"], f"Unsupported version : {version}" self.version = version elif line.startswith("SHEET "): self.sheet = line[len("SHEET "):].strip() elif line.startswith("IOPIN "): tag, posX, posY, direction = line.split() text = self.labels[-1] # Assuming it is the last FLAG parsed assert text.coord.X == int(posX) and text.coord.Y == int(posY), "Syntax Error, getting a IOPIN without an associated label" port = Port(text, direction) self.ports.append(port) # the following is identical to the code in asy_reader.py. If you modify it, do so in both places. elif line.startswith("LINE") or line.startswith("RECTANGLE") or line.startswith("CIRCLE"): # format: LINE|RECTANGLE|CIRCLE Normal, x1, y1, x2, y2, [line_style] # Maybe support something else than 'Normal', but LTSpice does not seem to do so. line_elements = line.split() assert len(line_elements) in (6, 7), "Syntax Error, line badly badly formatted" x1 = int(line_elements[2]) y1 = int(line_elements[3]) x2 = int(line_elements[4]) y2 = int(line_elements[5]) if line.startswith("LINE"): line = Line(Point(x1, y1), Point(x2, y2)) if len(line_elements) == 7: line.style.pattern = line_elements[6] self.lines.append(line) if line_elements[0] in ("RECTANGLE", "CIRCLE"): shape = Shape(line_elements[0], [Point(x1, y1), Point(x2, y2)]) if len(line_elements) == 7: shape.line_style.pattern = line_elements[6] self.shapes.append(shape) elif line.startswith("ARC"): # I don't support editing yet, so why make it complicated # format: ARC Normal, x1, y1, x2, y2, x3, y3, x4, y4 [line_style] # Maybe support something else than 'Normal', but LTSpice does not seem to do so. line_elements = line.split() assert len(line_elements) in (10, 11), "Syntax Error, line badly formatted" points = [Point(int(line_elements[i]), int(line_elements[i + 1])) for i in range(2, 9, 2)] arc = Shape("ARC", points) if len(line_elements) == 11: arc.line_style.pattern = line_elements[10] self.shapes.append(arc) elif line.startswith("DATAFLAG"): pass # DATAFLAG is the placeholder to show simulation information. It is ignored by AscEditor else: raise NotImplementedError("Primitive not supported for ASC file\n" f'"{line}"') if component is not None: assert component.reference is not None, "Component InstName was not given" self.components[component.reference] = component
def _get_symbol(self, symbol: str) -> AsyReader: asy_filename = symbol + os.path.extsep + "asy" if sys.platform == "linux" or sys.platform == "darwin": if '\\' in asy_filename: # This is a Windows path, so we need to remove the backslashes for non-Windows use asy_filename = asy_filename.replace('\\', '/') # and sometimes you have more than one asy_filename = asy_filename.replace('//', '/') # Escaped spaces asy_filename = asy_filename.replace('/ ', ' ') elif sys.platform == "win32": # Windows replaces spaces with \\<space> in filenames asy_filename = asy_filename.replace('\\ ', ' ') # and sometimes you have more than one asy_filename = asy_filename.replace('\\\\', '\\') asy_path = self._asy_file_find(asy_filename) if asy_path is None: raise FileNotFoundError(f"File {asy_filename} not found") answer = AsyReader(asy_path) return answer def _get_subcircuit(self, symbol: AsyReader) -> 'SpiceEditor | AscEditor': # two main possibilities here: # either the symbol refers to a library file, # either to a subcircuit in another .asc file. This appears to only happen with BLOCK symbols if symbol.symbol_type not in ("CELL", "BLOCK"): raise ValueError(f"Symbol type {symbol.symbol_type} not supported") lib = symbol.get_library() if lib is None and symbol.symbol_type == "BLOCK": asc_filename = symbol.get_schematic_file() if asc_filename.exists(): asc_path = asc_filename else: # TODO: should we add simulator_lib_paths to the search? asc_path = search_file_in_containers(asc_filename.stem + os.path.extsep + "asc", # file to search os.path.split(self.asc_file_path)[0], # The current script directory os.path.curdir, # The directory where the script is located *self.custom_lib_paths # The custom library paths. They are last here, contrary to other places... Why? ) if asc_path is None: raise FileNotFoundError(f"File {asc_filename} not found") answer = AscEditor(asc_path) elif lib is None and symbol.symbol_type == "CELL": # TODO: the library is often specified later on, so this may need to move. return None else: # load the model from the library model = symbol.get_model() lib_path = self._lib_file_find(lib) if lib_path is None: raise FileNotFoundError(f"File {lib} not found") answer = SpiceEditor.find_subckt_in_lib(lib_path, model) return answer
[docs] def get_subcircuit(self, reference: str) -> 'AscEditor': """Returns an AscEditor file corresponding to the symbol""" sub = self.get_component(reference) if '_SUBCKT' in sub.attributes: return sub.attributes['_SUBCKT'] raise AttributeError(f"An associated subcircuit was not found for {reference}")
[docs] def get_component_info(self, reference) -> dict: """Returns the reference information as a dictionary""" component = self.get_component(reference) info = {name: value for name, value in component.attributes.items() if not name.startswith("WINDOW ")} info["InstName"] = reference # For legacy purposes return info
[docs] def get_component_position(self, reference: str) -> tuple[Point, ERotation]: component = self.get_component(reference) return component.position, component.rotation
[docs] def set_component_position(self, reference: str, position: Point, rotation: ERotation) -> None: component = self.get_component(reference) component.position = position component.rotation = rotation
def _get_param_named(self, param_name): param_name_uppercase = param_name.upper() search_expression = re.compile(PARAM_REGEX(r"\w+"), re.IGNORECASE) for directive in self.directives: if directive.type == TextTypeEnum.COMMENT: continue # this is a comment, skip it if directive.text.upper().startswith(".PARAM"): matches = search_expression.finditer(directive.text) for match in matches: if match.group("name").upper() == param_name_uppercase: return match, directive return None, None
[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 directive in self.directives: if directive.type == TextTypeEnum.COMMENT: continue # this is a comment, skip it if directive.text.upper().startswith(".PARAM"): matches = search_expression.finditer(directive.text) for match in matches: param_name = match.group('name') param_names.append(param_name.upper()) return sorted(param_names)
[docs] def get_parameter(self, param: str) -> str: match, directive = self._get_param_named(param) if match: return match.group('value') else: raise ParameterNotFoundError(f"Parameter {param} not found in ASC file")
[docs] def set_parameter(self, param: str, value: ValueType) -> None: super().set_parameter(param, value) match, directive = self._get_param_named(param) if isinstance(value, (int, float)): value_str = format_eng(value) else: value_str = value if match: _logger.debug(f"Parameter {param} found in ASC file, updating it") start, stop = match.span('value') directive.text = f"{directive.text[:start]}{value_str}{directive.text[stop:]}" _logger.info(f"Parameter {param} updated to {value_str}") else: # Was not found so we need to add it, _logger.debug(f"Parameter {param} not found in ASC file, adding it") x, y = self._get_text_space() coord = Point(x, y) text = f".param {param}={value_str}" directive = Text(coord=coord, text=text, size=2, type=TextTypeEnum.DIRECTIVE) _logger.info(f"Parameter {param} added with value {value_str}") self.directives.append(directive) self.updated = True
[docs] def set_component_value(self, device: str, value: ValueType) -> None: """ Sets the value of the component :param device: The reference of the component :param value: The new value """ super().set_component_value(device, value) sub_circuit, ref = self._get_parent(device) if sub_circuit != self: # The component is in a subcircuit if isinstance(sub_circuit, SpiceCircuit): _logger.warning(f"Component {device} is in an Spice subcircuit. " f"This function may not work as expected.") return sub_circuit.set_component_value(ref, value) else: component = self.get_component(device) if "Value" in component.attributes: if isinstance(value, str): value_str = value else: value_str = format_eng(value) component.attributes["Value"] = value_str _logger.info(f"Component {device} updated to {value_str}") self.set_updated(device) else: _logger.error(f"Component {device} does not have a Value attribute") raise ComponentNotFoundError(f"Component {device} does not have a Value attribute")
[docs] def set_element_model(self, element: str, model: str) -> None: super().set_element_model(element, model) component = self.get_component(element) component.symbol = model _logger.info(f"Component {element} updated to {model}") self.set_updated(element)
[docs] def get_component_value(self, element: str) -> str: component = self.get_component(element) values = [component.attributes[param_name] for param_name in ["Value", "Value2"] if param_name in component.attributes] if len(values) == 0: _logger.error(f"Component {element} does not have a Value attribute") raise ComponentNotFoundError(f"Component {element} does not have a Value attribute") return ' '.join(values)
[docs] def get_component_parameters(self, element: str, as_dicts: bool = False) -> dict: """ Returns the parameters of a component that are related with Spice operation. That is: Value, Value2, SpiceModel, SpiceLine, SpiceLine2, plus all contents of SpiceLine, SpiceLine2 :param element: Reference of the circuit element to get the parameters. :param as_dicts: will report the contents of SpiceLine and SpiceLine2 inside a SpiceLine/SpiceLine2 instead of separately. :return: parameters of the circuit element in dictionary format. :raises: ComponentNotFoundError - In case the component is not found NotImplementedError - for not supported operations """ component = self.get_component(element) parameters = {} search_regex = re.compile(PARAM_REGEX(r'\w+'), re.IGNORECASE) for key, value in component.attributes.items(): if key in LTSPICE_PARAMETERS: parameters[key] = value if key in LTSPICE_PARAMETERS_REDUCED: # if we have a structured attribute, return the full dict of it # this is compatible with set_component_parameters sub_parameters = {} matches = search_regex.finditer(value) # This might contain one or more parameters for match in matches: sub_parameters[match.group("name")] = try_convert_value(match.group("value")) if sub_parameters: if as_dicts: parameters[key] = sub_parameters else: parameters.update(sub_parameters) return parameters
[docs] def set_component_parameters(self, element: str, **kwargs) -> None: """ Sets the parameters of a component that are related with Spice operation. That is: Value, Value2, SpiceModel, SpiceLine, SpiceLine2, or any parameters are or could be in SpiceLine, SpiceLine2. Unknown parameters will be added to SpiceLine. Setting None removes the parameter if possible. Usage 1: :: editor.set_component_parameters(R1, value=330, temp=25) Usage 2: :: value_settings = {'value': 330, 'temp': 25} editor.set_component_parameters(R1, **value_settings) :param element: Reference of the circuit element. :key <param_name>: The key is the parameter name and the value is the value to be set. Values can either be strings; integers or floats. When None is given, the parameter will be removed, if possible. :return: Nothing :raises: ComponentNotFoundError - In case one of the component is not found. """ super().set_component_parameters(element, **kwargs) component = self.get_component(element) for key, value in kwargs.items(): # format the value if value is None: value_str = None elif isinstance(value, str): value_str = value.strip() else: value_str = format_eng(value) params = self.get_component_parameters(element, as_dicts=True) if key in params: # I only have the LTSPICE_PARAMETERS as keys here, so when I match, i can overwrite # I do not support delete here, as some of the keys are mandatory component.attributes[key] = value_str _logger.info(f"Component {element} updated with parameter {key}:{value}") else: foundme = False # not found: look in the second level dicts for param_key in LTSPICE_PARAMETERS_REDUCED: if param_key in params: if key in params[param_key]: # found in the dict # update the dict if value_str is None: # remove if empty params[param_key].pop(key) else: params[param_key][key] = value_str # and make the line out of the dict component.attributes[param_key] = ' '.join([f'{p_key}={p_value}' for p_key, p_value in params[param_key].items()]) _logger.info(f"Component {element} updated with parameter {key}:{value_str}") foundme = True if not foundme: if value_str is not None: # don't add if there's nothing to add if key in LTSPICE_PARAMETERS: # known parameter, set the value component.attributes[key] = value_str _logger.info(f"Component {element} updated with parameter {key}:{value_str}") else: # nothing found, and not a known parameter, put it in SpiceLine param_key = LTSPICE_PARAMETERS_REDUCED[0] if param_key in params: # if SpiceLine exists: add to the dict params[param_key][key] = value_str # and make the line out of the dict component.attributes[param_key] = ' '.join([f'{p_key}={p_value}' for p_key, p_value in params[param_key].items()]) else: # if SpiceLine does not exist: create the line component.attributes[param_key] = f'{key}={value_str}' _logger.info(f"Component {element} updated with parameter {key}:{value_str}") self.set_updated(element)
[docs] def get_components(self, prefixes='*') -> list: if prefixes == '*': return list(self.components.keys()) return [k for k in self.components.keys() if k[0] in prefixes]
[docs] def remove_component(self, designator: str): super().remove_component(designator) sub_circuit, ref = self._get_parent(designator) del sub_circuit.components[ref] sub_circuit.updated = True
def _get_text_space(self): """ Returns the coordinate on the Schematic File canvas where a text can be appended. """ min_x = 100000 # High enough to be sure it will be replaced max_x = -100000 min_y = 100000 # High enough to be sure it will be replaced max_y = -100000 _, x, y = self.sheet.split() min_x = min(min_x, int(x)) min_y = min(min_y, int(y)) for wire in self.wires: min_x = min(min_x, wire.V1.X, wire.V2.X) max_x = max(max_x, wire.V1.X, wire.V2.X) min_y = min(min_y, wire.V1.Y, wire.V2.Y) max_y = max(max_y, wire.V1.Y, wire.V2.Y) for flag in self.labels: min_x = min(min_x, flag.coord.X) max_x = max(max_x, flag.coord.X) min_y = min(min_y, flag.coord.Y) max_y = max(max_y, flag.coord.Y) for directive in self.directives: min_x = min(min_x, directive.coord.X) max_x = max(max_x, directive.coord.X) min_y = min(min_y, directive.coord.Y) max_y = max(max_y, directive.coord.Y) for component in self.components.values(): min_x = min(min_x, component.position.X) max_x = max(max_x, component.position.X) min_y = min(min_y, component.position.Y) max_y = max(max_y, component.position.Y) return min_x, max_y + 24 # Setting the text in the bottom left corner of the canvas
[docs] def add_library_paths(self, *paths): """ .. deprecated:: 1.1.4 Use the class method `set_custom_library_paths()` instead. Adding paths for searching for symbols and libraries""" self.set_custom_library_paths(*paths)
def _lib_file_find(self, filename) -> str | None: # create list of directories to search, based on the simulator_lib_paths. Just add "/sub" to the path my_lib_paths = [os.path.join(x, "sub") for x in self.simulator_lib_paths] # find the file file_found = search_file_in_containers(filename, os.path.split(self.asc_file_path)[0], # The directory where the file is located os.path.curdir, # The current script directory, *my_lib_paths, # The simulator's library paths, adapted for the occasion *self.custom_lib_paths, os.path.expanduser("~/AppData/Local/Programs/ADI/LTspice/lib.zip") # TODO: is this needed? This risk being outdated ) return file_found def _asy_file_find(self, filename) -> str | None: if filename in self.symbol_cache: return self.symbol_cache[filename] _logger.info(f"Searching for symbol {filename}...") # create list of directories to search, based on the simulator_lib_paths. Just add "/sym" to the path my_lib_paths = [os.path.join(x, "sym") for x in self.simulator_lib_paths] # find the file file_found = search_file_in_containers(filename, os.path.split(self.asc_file_path)[0], # The directory where the file is located os.path.curdir, # The current script directory, *my_lib_paths, # The simulator's library paths, adapted for the occasion *self.custom_lib_paths ) if file_found is not None: self.symbol_cache[filename] = file_found return file_found
[docs] def add_instruction(self, instruction: str) -> None: # docstring inherited from BaseEditor instruction = instruction.strip() # Clean any end of line terminators set_command = instruction.split()[0].upper() if set_command in UNIQUE_SIMULATION_DOT_INSTRUCTIONS: # Before adding new instruction, if it is a unique instruction, we just replace it i = 0 while i < len(self.directives): directive = self.directives[i] if directive.type == TextTypeEnum.COMMENT: i += 1 continue # this is a comment directive_command = directive.text.split()[0].upper() if directive_command in UNIQUE_SIMULATION_DOT_INSTRUCTIONS: super().remove_instruction(self.directives[i].text) self.directives[i].text = instruction self.updated = True super().add_instruction(instruction) return # Job done, can exit this method i += 1 elif set_command.startswith('.PARAM'): raise RuntimeError('The .PARAM instruction should be added using the "set_parameter" method') else: super().add_instruction(instruction) # If we get here, then the instruction was not found, so we need to add it x, y = self._get_text_space() coord = Point(x, y) directive = Text(coord=coord, text=instruction, size=2, type=TextTypeEnum.DIRECTIVE) self.directives.append(directive) self.updated = True
[docs] def remove_instruction(self, instruction: str) -> bool: i = 0 while i < len(self.directives): if self.directives[i].type == TextTypeEnum.COMMENT: i += 1 continue # this is a comment if instruction in self.directives[i].text: text = self.directives[i].text del self.directives[i] _logger.info(f"Instruction {text} removed") self.updated = True super().remove_instruction(instruction) return True # Job done, can exit this method i += 1 msg = f'Instruction "{instruction}" not found' _logger.error(msg) return False
[docs] def remove_Xinstruction(self, search_pattern: str) -> bool: regex = re.compile(search_pattern, re.IGNORECASE) instr_removed = False i = 0 while i < len(self.directives): if self.directives[i].type == TextTypeEnum.COMMENT: i += 1 continue # this is a comment instruction = self.directives[i].text if regex.match(instruction) is not None: instr_removed = True del self.directives[i] super().remove_instruction(instruction) _logger.info(f"Instruction {instruction} removed") else: i += 1 if instr_removed: self.updated = True return True else: msg = f'Instructions matching "{search_pattern}" not found' _logger.error(msg) return False