Source code for spicelib.editor.spice_editor

#!/usr/bin/env python
# -------------------------------------------------------------------------------
#
#  ███████╗██████╗ ██╗ ██████╗███████╗██╗     ██╗██████╗
#  ██╔════╝██╔══██╗██║██╔════╝██╔════╝██║     ██║██╔══██╗
#  ███████╗██████╔╝██║██║     █████╗  ██║     ██║██████╔╝
#  ╚════██║██╔═══╝ ██║██║     ██╔══╝  ██║     ██║██╔══██╗
#  ███████║██║     ██║╚██████╗███████╗███████╗██║██████╔╝
#  ╚══════╝╚═╝     ╚═╝ ╚═════╝╚══════╝╚══════╝╚═╝╚═════╝
#
# Name:        spice_editor.py
# Purpose:     Class made to update Generic Spice netlists
#
# Author:      Nuno Brum (nuno.brum@gmail.com)
#
# License:     refer to the LICENSE file
# -------------------------------------------------------------------------------

from __future__ import annotations

import logging

from pathlib import Path

from .spice_file import SpiceFile
from ..sim.process_callback import CallbackType

from .base_editor import BaseEditor
from .updates import UpdateType, UpdatePermission
from .spice_subcircuit import SpiceCircuit, ControlEditor

from ..utils.detect_encoding import detect_encoding, EncodingDetectError

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

__author__ = "Nuno Canto Brum <nuno.brum@gmail.com>"
__copyright__ = "Copyright 2021, Fribourg Switzerland"



# A Spice netlist can only have one of the instructions below, otherwise an error will be raised

# All the regular expressions here may or may not include leading or trailing spaces
# This means that when you re-assemble parts, you need to be careful to preserve spaces when needed.
# See _insert_section()


# component_replace_regexs = {prefix: re.compile(pattern, re.IGNORECASE) for prefix, pattern in REPLACE_REGEXS.items()}

# The following variable deprecated, and here only so that people can find it.
# It is replaced by SpiceEditor.set_custom_library_paths().
# Since I cannot keep it operational easily, I do not use the deprecated decorator or the magic from https://stackoverflow.com/a/922693.
#
# LibSearchPaths = []


[docs] class SpiceEditor(SpiceFile): """ Provides interfaces to manipulate SPICE netlist files. The class doesn't update the netlist file itself. After implementing the modifications, the user should call the "save_netlist" method to write a new netlist file. :param netlist_file: Name of the .NET file to parse :param encoding: Forcing the encoding to be used on the circuit netlile read. Defaults to 'autodetect' which will call a function that tries to detect the encoding automatically. This, however, is not 100% foolproof. :keyword create_blank: Create a blank '.net' file when 'netlist_file' not exist. False by default :keyword include_file: If an include file is being parsed, the control of the ending .END statement is suppressed. """ def __init__(self, netlist_file: Path | str, encoding='autodetect', **kwargs): if kwargs.get('create_blank', False): if encoding == 'autodetect': encoding = 'utf-8' else: encoding = encoding else: if encoding == 'autodetect': try: encoding = detect_encoding(netlist_file, r'^(?:\*|\.title)') # Normally, the file will start with a '*' except for KiCad that can start with '.title' except EncodingDetectError as err: raise err super().__init__(netlist_file, encoding, **kwargs)
[docs] def reset_netlist(self, **kwargs) -> bool: """ Reset the netlist state and reload or reinitialize its content. :keyword create_blank: Create a blank '.net' file when 'netlist_file' not exist. :keyword include_file: If an include file is being parsed, the control of the ending .END statement is suppressed. This is useful when parsing include files, which do not have an .END statement, but are just a part of the netlist. :return: True if successful, False otherwise. """ finished = super().reset_netlist(**kwargs) if kwargs.get('create_blank', False): self._add_lines(['.END']) else: if not finished: raise SyntaxError("Netlist with missing .END or .ENDS statements") if not self.custom_lib_paths: # See if it can find a comment specifying who generated this netlist, only checks the first # 5 lines lib_paths = None for line in self.netlist[:5]: if isinstance(line, str): line_stripped_upped = line.strip().upper() if line.startswith('*'): if line_stripped_upped.endswith(".ASC"): from ..simulators.ltspice_simulator import LTspice lib_paths = LTspice.get_default_library_paths() _logger.info(f"Found LTspice netlist pattern.\nAdding search paths: [{lib_paths}]") break elif line_stripped_upped.endswith(".QSCH"): from ..simulators.qspice_simulator import Qspice lib_paths = Qspice.get_default_library_paths() _logger.info(f"Found Qspice netlist pattern.\nAdding search paths: [{lib_paths}]") break elif 'XYCE' in line_stripped_upped: from ..simulators.xyce_simulator import XyceSimulator lib_paths = XyceSimulator.get_default_library_paths() _logger.info(f"Found Xyce netlist pattern.\nAdding search paths: [{lib_paths}]") break elif 'NGSPICE' in line_stripped_upped: from ..simulators.ngspice_simulator import NGspiceSimulator lib_paths = NGspiceSimulator.get_default_library_paths() _logger.info(f"Found NGspice netlist pattern.\nAdding search paths: [{lib_paths}]") break if lib_paths: self.set_custom_library_paths(lib_paths) return finished
[docs] def get_control_sections(self) -> list[str]: """ Returns a list representing the control sections in the netlist. Control sections are all anonymous, so they do not have a name, just an index. They are also not parsed, they are just a list of strings (with embedded newlines). :return: list of control section strings. These strings have each multiple lines, start with ``.CONTROL`` and end with ``.ENDC``. """ control_sections = [] for line in self.netlist: if isinstance(line, ControlEditor): control_sections.append(line.content) return control_sections
[docs] def add_control_section(self, instruction: str) -> None: """ Adds a control section to the netlist. The instruction should be a multi-line string that starts with '.CONTROL' and ends with '.ENDC'. It will be added as a ControlEditor object to the netlist. You can also use the `add_instruction()` method, but that method has less checking of the format. :param instruction: control section instruction :raises ValueError: if the instruction does not start with ``.CONTROL`` or does not end with ``.ENDC`` """ instruction = instruction.strip() if not instruction.upper().startswith('.CONTROL') or not instruction.upper().endswith('.ENDC'): raise ValueError("Control section must start with '.CONTROL' and end with '.ENDC'") self.add_instruction(instruction)
[docs] def remove_control_section(self, index: int = 0) -> bool: """ Removes a control section from the netlist, based on the index in `get_control_sections()`. You can also use `remove_instruction()`, but there, the given text must match the entire control section. :param index: index of the control section to remove, according to `get_control_sections()` :returns: True if the control section was found and removed, False otherwise """ permission = self.begin_update() if permission == UpdatePermission.Deny: raise PermissionError('The .NET file is read-only') if index < 0: raise IndexError("Control section index out of range") i = 0 for nr, line in enumerate(self.netlist): if isinstance(line, ControlEditor): if i == index: del self.netlist[nr] logtxt = line.content.replace("\r", "\\r").replace("\n", "\\n") if permission == UpdatePermission.Inform: self.end_update('INSTRUCTION', logtxt, UpdateType.DeleteInstruction) _logger.info(f"Control section {index} removed") return True i += 1 _logger.error(f"Control section {index} was not found") return False
[docs] def run(self, wait_resource: bool = True, callback: CallbackType | None = None, timeout: float | None = None, run_filename: str | None = None, simulator=None): """ .. deprecated:: 1.0 Use the `run` method from the `SimRunner` class instead. Convenience function for maintaining legacy with legacy code. Runs the SPICE simulation. """ from ..sim.sim_runner import SimRunner runner = SimRunner(simulator=simulator) return runner.run(self, wait_resource=wait_resource, callback=callback, timeout=timeout, run_filename=run_filename)
[docs] @classmethod def add_library_search_paths(cls, *paths) -> None: """ .. deprecated:: 1.1.4 Use the class method `set_custom_library_paths()` instead. Adds search paths for libraries. By default, the local directory and the ~username/"Documents/LTspiceXVII/lib/sub will be searched forehand. Only when a library is not found in these paths then the paths added by this method will be searched. :param paths: Path to add to the Search path :type paths: str :return: Nothing """ cls.set_custom_library_paths(*paths)