Source code for PyLTSpice.editor.asc_editor

#!/usr/bin/env python
# coding=utf-8
# -------------------------------------------------------------------------------
#    ____        _   _____ ____        _
#   |  _ \ _   _| | |_   _/ ___| _ __ (_) ___ ___
#   | |_) | | | | |   | | \___ \| '_ \| |/ __/ _ \
#   |  __/| |_| | |___| |  ___) | |_) | | (_|  __/
#   |_|    \__, |_____|_| |____/| .__/|_|\___\___|
#          |___/                |_|
#
# 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
# -------------------------------------------------------------------------------
from pathlib import Path
from typing import Union, Tuple, List
import re
import logging
from .base_editor import BaseEditor, format_eng, ComponentNotFoundError, ParameterNotFoundError, PARAM_REGEX, \
    UNIQUE_SIMULATION_DOT_INSTRUCTIONS

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

TEXT_REGEX = re.compile(r"TEXT (-?\d+)\s+(-?\d+)\s+(Left|Right|Top|Bottom)\s\d+\s*(?P<type>[!;])(?P<text>.*)",
                        re.IGNORECASE)
TEXT_REGEX_X = 1
TEXT_REGEX_Y = 2
TEXT_REGEX_ALIGN = 3
TEXT_REGEX_TYPE = 4
TEXT_REGEX_TEXT = 5

END_LINE_TERM = "\n"


[docs]class AscEditor(BaseEditor): """Class made to update directly the ltspice ASC files""" def __init__(self, asc_file: str): self._asc_file_path = Path(asc_file) self._asc_file_lines: List[str] = [] self._symbols = {} self._texts = [] # This is only here to avoid cycling over the netlist everytime we need to retrieve the texts if not self._asc_file_path.exists(): raise FileNotFoundError(f"File {asc_file} not found") # read the file into memory self.reset_netlist() @property def circuit_file(self) -> Path: return self._asc_file_path
[docs] def write_netlist(self, run_netlist_file: Union[str, Path]) -> None: if isinstance(run_netlist_file, str): run_netlist_file = Path(run_netlist_file) run_netlist_file = run_netlist_file.with_suffix(".asc") with open(run_netlist_file, 'w') as asc_file: _logger.info(f"Writing ASC file {run_netlist_file}") asc_file.writelines(self._asc_file_lines)
[docs] def reset_netlist(self): with open(self._asc_file_path, 'r') as asc_file: _logger.info(f"Reading ASC file {self._asc_file_path}") self._asc_file_lines = asc_file.readlines() self._parse_asc_file()
def _parse_asc_file(self): symbol_attributes = {} self._symbols.clear() self._texts.clear() _logger.debug("Parsing ASC file") for line_no, line in enumerate(self._asc_file_lines): if line.startswith("SYMBOL"): tokens = line.split() if symbol_attributes: self._symbols[symbol_attributes["InstName"]] = symbol_attributes symbol_attributes = {'Name': tokens[1], 'line': line_no} elif line.startswith("SYMATTR"): tokens = line.split() if symbol_attributes: symbol_attributes[tokens[1]] = tokens[2] elif line.startswith("TEXT"): match = TEXT_REGEX.match(line) if match and match.group(TEXT_REGEX_TYPE) == "!": text = match.group(TEXT_REGEX_TEXT) self._texts.append((line_no, text.strip())) if symbol_attributes: self._symbols[symbol_attributes["InstName"]] = symbol_attributes
[docs] def get_component_info(self, component) -> dict: """Returns the component information as a dictionary""" if component not in self._symbols: _logger.error(f"Component {component} not found in ASC file") raise ComponentNotFoundError(f"Component {component} not found in ASC file") return self._symbols[component]
def _get_line_matching(self, command, search_expression: re.Pattern) -> Tuple[int, Union[re.Match, None]]: command_upped = command.upper() for line_no, line in self._texts: if line.upper().startswith(command_upped): match = search_expression.search(line) if match: return line_no, match else: return -1, None
[docs] def get_parameter(self, param: str) -> str: param_regex = re.compile(PARAM_REGEX % param, re.IGNORECASE) line_no, match = self._get_line_matching(".PARAM", param_regex) 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: Union[str, int, float]) -> None: param_regex = re.compile(PARAM_REGEX % param, re.IGNORECASE) line_no, match = self._get_line_matching(".PARAM", param_regex) if match: _logger.debug(f"Parameter {param} found in ASC file, updating it") if isinstance(value, (int, float)): value_str = format_eng(value) else: value_str = value line: str = self._asc_file_lines[line_no] match = param_regex.search(line) # repeating the search, so we update the correct start/stop parameter start, stop = match.span(param_regex.groupindex['replace']) self._asc_file_lines[line_no] = line[:start] + "{}={}".format(param, value_str) + line[stop:] _logger.info(f"Parameter {param} updated to {value_str}") _logger.debug(f"Line:{line_no + 1} Updated to: {self._asc_file_lines[line_no]}") 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() self._asc_file_lines.append("TEXT {} {} Left 2 !.param {}={}".format(x, y, param, value) + END_LINE_TERM) _logger.info(f"Parameter {param} added with value {value}") _logger.debug(f"Line:{len(self._asc_file_lines)} Added: {self._asc_file_lines[-1]}") self._parse_asc_file()
[docs] def set_component_value(self, device: str, value: Union[str, int, float]) -> None: comp_info = self.get_component_info(device) start = comp_info['line'] for offset, line in enumerate(self._asc_file_lines[start:]): if line.startswith("SYMATTR Value"): if isinstance(value, str): value_str = value else: value_str = format_eng(value) self._asc_file_lines[start + offset] = "SYMATTR Value {}".format(value_str) + END_LINE_TERM _logger.info(f"Component {device} updated to {value_str}") _logger.debug(f"Line:{start + offset + 1} Updated to: {self._asc_file_lines[start + offset]}") self._parse_asc_file() break 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: comp_info = self.get_component_info(element) line_no = comp_info['line'] tokens = self._asc_file_lines[line_no].split(' ') tokens[1] = model self._asc_file_lines[line_no] = ' '.join(tokens) _logger.info(f"Component {element} updated to {model}")
[docs] def get_component_value(self, element: str) -> str: comp_info = self.get_component_info(element) if "Value" not in comp_info: _logger.error(f"Component {element} does not have a Value attribute") raise ComponentNotFoundError(f"Component {element} does not have a Value attribute") return comp_info["Value"]
[docs] def get_components(self, prefixes='*') -> list: if prefixes == '*': return list(self._symbols.keys()) return [k for k in self._symbols.keys() if k[0] in prefixes]
[docs] def remove_component(self, designator: str): comp_info = self.get_component_info(designator) line_end = line_start = comp_info['line'] for offset, line in enumerate(self._asc_file_lines[line_start:]): if line.startswith("SYMBOL") or line.startswith("WINDOW") or line.startswith("SYMATTR"): continue else: line_end = line_start + offset break # If another line is found, then it is the start of another component del self._asc_file_lines[line_start:line_end] self._parse_asc_file()
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 = 0 min_y = 100000 # High enough to be sure it will be replaced max_y = 0 for line in self._asc_file_lines: if line.startswith("SHEET"): x, y = line.split()[2:4] min_x = int(x) min_y = int(y) elif line.startswith("WIRE"): x1, y1, x2, y2 = [int(x) for x in line.split()[1:5]] min_x = min(min_x, x1, x2) max_x = max(max_x, x1, x2) min_y = min(min_y, y1, y2) max_y = max(max_y, y1, y2) elif line.startswith("FLAG") or line.startswith("TEXT"): x1, y1 = [int(x) for x in line.split()[1:3]] min_x = min(min_x, x1) max_x = max(max_x, x1) min_y = min(min_y, y1) max_y = max(max_y, y1) elif line.startswith("SYMBOL"): x1, y1 = [int(x) for x in line.split()[2:4]] min_x = min(min_x, x1) max_x = max(max_x, x1) min_y = min(min_y, y1) max_y = max(max_y, y1) return min_x, max_y + 24 # Setting the text in the bottom left corner of the canvas
[docs] def add_instruction(self, instruction: str) -> None: instruction = instruction.strip() # Clean any end of line terminators command = instruction.split()[0].upper() if 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._texts): line_no, line = self._texts[i] command = line.split()[0].upper() if command in UNIQUE_SIMULATION_DOT_INSTRUCTIONS: line = self._asc_file_lines[line_no] self._asc_file_lines[line_no] = line[:line.find("!")] + instruction + END_LINE_TERM self._parse_asc_file() return # Job done, can exit this method i += 1 elif command.startswith('.PARAM'): raise RuntimeError('The .PARAM instruction should be added using the "set_parameter" method') # If we get here, then the instruction was not found, so we need to add it x, y = self._get_text_space() self._asc_file_lines.append("TEXT {} {} Left 2 !{}".format(x, y, instruction) + END_LINE_TERM) self._parse_asc_file()
[docs] def remove_instruction(self, instruction: str) -> None: i = 0 while i < len(self._texts): line_no, line = self._texts[i] if instruction in line: del self._asc_file_lines[line_no] self._parse_asc_file() return # Job done, can exit this method i += 1 _logger.error(f'Instruction "{instruction}" not found') raise RuntimeError(f'Instruction "{instruction}" not found')