Source code for spicelib.log.qspice_log_reader

#!/usr/bin/env python

# -------------------------------------------------------------------------------
#
#  ███████╗██████╗ ██╗ ██████╗███████╗██╗     ██╗██████╗
#  ██╔════╝██╔══██╗██║██╔════╝██╔════╝██║     ██║██╔══██╗
#  ███████╗██████╔╝██║██║     █████╗  ██║     ██║██████╔╝
#  ╚════██║██╔═══╝ ██║██║     ██╔══╝  ██║     ██║██╔══██╗
#  ███████║██║     ██║╚██████╗███████╗███████╗██║██████╔╝
#  ╚══════╝╚═╝     ╚═╝ ╚═════╝╚══════╝╚══════╝╚═╝╚═════╝
#
# Name:        qspice_log_reader.py
# Purpose:     Read measurement data from a qspice log file
#
# Author:      Nuno Brum (nuno.brum@gmail.com)
#
# Created:     24-09-2023
# License:     refer to the LICENSE file
# -------------------------------------------------------------------------------
import re
import logging
from pathlib import Path

from .logfile_data import LogfileData, try_convert_value, split_line_into_values
from ..sim.simulator import run_function
from ..simulators.qspice_simulator import Qspice

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


[docs] class QspiceLogReader(LogfileData): """ Reads an QSpice log file and retrieves the step and measurement information if it exists. The step information is then accessible by using the 'stepset' property of this class. This class is intended to be used together with the RawRead to retrieve the runs that are associated with a given parameter setting. This class constructor only reads the step information of the log file. If the measures are needed, then the user should call the get_measures() method. :property stepset: dictionary in which the keys are the variables that were STEP'ed during the simulation and the associated value is a list representing the sequence of assigned values during simulation. :property headers: list containing the headers on the exported data. This is only populated when the *read_measures* optional parameter is set to False. :property dataset: dictionary in which the keys are the the headers and the export file and the values are lists. This is information is only populated when the *read_measures* optional parameter is set to False. :param log_filename: path to the Export file. :type log_filename: str :param read_measures: Optional parameter to skip measuring data reading. :type read_measures: boolean :param step_set: Optional parameter to provide the steps from another file. This is used to process .mout files. :type step_set: dict """ def __init__(self, log_filename: str, read_measures=True, step_set: dict = None, encoding=None): super().__init__(step_set) self.logname = Path(log_filename) if encoding is None: self.encoding = 'utf-8' else: self.encoding = encoding step_regex = re.compile(r"^\s*(\d+) of \d+ steps:\s+\.step (.*)$") _logger.debug(f"Processing LOG file:{log_filename}") with open(log_filename, encoding=self.encoding) as fin: line = fin.readline() while line: match = step_regex.match(line) if match: self.step_count += 1 step = int(match.group(1)) stepset = match.group(2) assert self.step_count == step, f"Step count mismatch: {self.step_count} != {step}" _logger.debug(f"Found step {step} with stepset {stepset}") tokens = stepset.strip('\r\n').split(' ') for tok in tokens: if '=' not in tok: continue lhs, rhs = tok.split("=") # Try to convert to int or float rhs = try_convert_value(rhs) lhs = lhs.lower() ll = self.stepset.get(lhs, None) if ll: ll.append(rhs) else: self.stepset[lhs] = [rhs] line = fin.readline() if read_measures: meas_file = self.obtain_measures() self.parse_meas_file(meas_file)
[docs] def obtain_measures(self, meas_filename: Path = None) -> Path: """ In QSpice the measures are obtained by calling the QPOST command giving as arguments the .qraw file and the .log file This function makes this call to QPOST and returns the measurement output file path. Note the call to QPOST includes the path to the circuit netlist. This is assumed to be the name of the logfile, but with the '.net' or '.cir' extension. :param meas_filename: This optional parameter specifies the measurement file name. If not given, it will assume the name of the log file but with the extension '.meas'. :returns: The .meas file path """ if meas_filename is None: meas_filename = self.logname.with_suffix(".meas") elif not isinstance(meas_filename, Path): meas_filename = Path(meas_filename) if not Qspice.is_available(): _logger.error("================== ALERT! ====================") _logger.error("Unable to find the QSPICE executable.") _logger.error("A specific location of the QSPICE can be set") _logger.error("using the create_from(<location>) class method") _logger.error("==============================================") raise RuntimeError("QSPICE not found in the usual locations. Please install it and try again.") # Get the QPOST location, which is the same as the QSPICE location qpost = [Qspice.spice_exe[0].replace("QSPICE64.exe", "QPOST.exe")] # Guess the name of the .net file netlist = self.logname.with_suffix('.net').absolute() if not Path.exists(netlist): netlist = self.logname.with_suffix('.cir').absolute() # Run the QPOST command cmd_run = qpost + [netlist, "-o", meas_filename.absolute()] _logger.debug(f"Running QPOST command: {cmd_run}") run_function(cmd_run) return meas_filename
[docs] def parse_meas_file(self, meas_filename): """ Parses the .meas file and reads all measurements contained in the file. Access to the measurements is done using this class interface. :param meas_filename: path to the measurement file to parse. :type meas_filename: str or pathlib.Path :returns: Nothing """ meas_regex = re.compile(r"^\.meas (\w+) (\w+) (.*)$") meas_name = None headers = None with open(meas_filename, encoding=self.encoding) as fin: line = fin.readline() while line: match = meas_regex.match(line) if match: headers = None token1 = match.group(1) token2 = match.group(2) if token1 in ('tran', 'ac', 'dc', 'op'): sim_type = token1 meas_name = token2 else: sim_type = token2 meas_name = token1 meas_expr = match.group(3) _logger.debug(f"Found measure {meas_name} of type {sim_type} with expression {meas_expr}") else: if meas_name: values = split_line_into_values(line) if headers is None: if self.has_steps(): headers = ['step'] + [meas_name + "_" + str(i) for i in range(len(values) - 1)] headers[1] = meas_name # first column is the measure name without _0 else: headers = [meas_name + "_" + str(i) for i in range(len(values))] headers[0] = meas_name # first column is the measure name without _0 for title in headers: self.dataset[title.lower()] = [] self.measure_count += 1 for k, title in enumerate(headers): self.dataset[title.lower()].append(values[k]) line = fin.readline()