diff --git a/qucs/python/parse_result.py b/qucs/python/parse_result.py index aaebcba0..ae2a74ab 100644 --- a/qucs/python/parse_result.py +++ b/qucs/python/parse_result.py @@ -1,131 +1,119 @@ +from dataclasses import dataclass, field +from pathlib import Path +from typing import Literal, TypeAlias import re + import numpy as np +VariableType: TypeAlias = Literal["indep", "dep"] +ComplexArray: TypeAlias = np.typing.NDArray[np.complex128] +FloatArray: TypeAlias = np.typing.NDArray[np.float64] +DataDict: TypeAlias = dict[str, ComplexArray | FloatArray] +VariableDict: TypeAlias = dict[str, VariableType] + + +@dataclass class QucsDataset: - def __init__(self, name: str) -> None: - ''' - A class for parsing and working with QUCS-S simulation results. - - Methods: - results - returns simulation result for given simulation variable - variables - prints the list of variables and their types - - Args: - name (str): Path to the QUCS-S dataset file - - Raises: - FileNotFoundError: If the specified dataset file is not found - ValueError: If data does not contain the expected variables - ValueError: If QUCS-S dataset is invalid - - Attributes: - _variables (Dict[str, str]): - A dictionary mapping variable names to either 'indep' or 'dep'. - __data (Dict[str, np.ndarray]): - A dictionary mapping variable names to arrays of values. - ''' + """A class for parsing and working with QUCS-S simulation results.""" + + file_path: str | Path + _variables: VariableDict = field(default_factory=dict, init=False) + _data: DataDict = field(default_factory=dict, init=False) + _qucs_dataset: list[str] = field(default_factory=list, init=False) + + def __post_init__(self) -> None: + """Initialize the dataset by reading and parsing the file.""" try: - with open(name, 'r') as f: + with open(self.file_path, "r", encoding="utf-8") as f: first_line = f.readline() if not first_line.startswith(" dict: - ''' - Parses a *.dat file containing QUCS-S simulation results. - - Returns: - dict: A dictionary of the variables in the dataset and their values - ''' - data = {} - numpoints = 1 - indep_count = 0 - shape = [] - ind = 0 - - for line in self.__qucs_dataset: - if line.startswith('<'): - if line.startswith('', line) - name = matched.group(2) - - # work with several independent variables + raise FileNotFoundError(f"QUCS-S dataset {self.file_path} not found.") + + self._parse_qucs_result() + + def _parse_qucs_result(self) -> None: + """Parse a *.dat file containing QUCS-S simulation results.""" + numpoints: int = 1 + indep_count: int = 0 + shape: int = 1 + ind: int = 0 + current_var: str = "" + + for line in self._qucs_dataset: + if line.startswith("<"): + if line.startswith("\w+) (?P\S+) (?P\d+)>", line) + if not matched: + continue + + current_var = matched.group('var') + points = int(matched.group('points')) + if indep_count >= 1: - # keeps the total number of points - numpoints = numpoints * int(matched.group(3)) - shape = int(matched.group(3)) + numpoints *= points + shape = points else: - # only parse the total number of points - numpoints = int(matched.group(3)) - shape = 1 + numpoints = points - # reserve an array for the values - data[name] = np.zeros(numpoints) + self._data[current_var] = np.zeros(numpoints) + self._variables[current_var] = "indep" + indep_count += 1 ind = 0 - # save that this variable is independent - self._variables[name] = 'indep' - indep_count += 1 - elif line.startswith('\S+)", line) + if not matched: + continue + + current_var = matched.group('var') + self._data[current_var] = np.zeros(numpoints, dtype=np.complex128) + self._variables[current_var] = "dep" ind = 0 - # store that this variable is dependent - self._variables[name] = 'dep' - else: - jind = line.find('j') - if jind == -1: - val = float(line) - else: - # complex number -> break into re/im part - val_re = line[0:jind-1] - sign = line[jind-1] - val_im = sign + line[jind+1:-1] - # complex number -> break into re/im part - val = complex(float(val_re), float(val_im)) - - # store the extracted datapoint - data[name][ind] = val + + elif ( + line.strip() and current_var + ): # Проверяем что строка не пустая и переменная определена + val = self._parse_value(line.strip()) + self._data[current_var][ind] = val ind += 1 - # here comes the clever trick :-) - # if a dependent variable depends on N > 1 (independent) variables, - # we reshape the vector we have obtained so far into an N-dimensional - # matrix - for key in self._variables: - if self._variables[key] == 'dep' and shape != 1: - data[key] = data[key].reshape(shape, int(data[key].size/shape)) - print(f'Simulation results for variable {key} reshaped into an N-dimensional matrix') - return data - - def results(self, variable_name: str) -> np.ndarray: - ''' - Returns simulation results for given variable name. - - Args: - variable_name (str): Name of the simulation variable - - Returns: - np.ndarray: Array of simulation results for the specified - variable name - ''' + # Reshape dependent variables for N > 1 independent variables + if shape > 1: + for key, var_type in self._variables.items(): + if var_type == "dep": + rows = shape + cols = int(self._data[key].size / shape) + self._data[key] = self._data[key].reshape(rows, cols) + print( + f"Simulation results for variable {key} reshaped into an N-dimensional matrix" + ) + + @staticmethod + def _parse_value(line: str) -> complex | float: + """Parse a string value into either a complex number or float.""" + jind = line.find("j") + if jind == -1: + return float(line) + + val_re = line[0:jind - 1] + sign = line[jind - 1] + val_im = sign + line[jind + 1:] + return complex(float(val_re), float(val_im)) + + def results(self, variable_name: str) -> ComplexArray | FloatArray: + """Return simulation results for given variable name.""" if variable_name not in self._variables: raise ValueError("Data does not contain the expected variables.") - return self.__data[variable_name] + return self._data[variable_name] def variables(self) -> None: - ''' - Prints the variables in the dataset and their types to the console. - ''' - print('Variables: ') + """Print the variables in the dataset and their types to the console.""" + print("Variables: ") for name, vtype in self._variables.items(): print(f" {name}: {vtype}") + diff --git a/qucs/python/parse_result_example.py b/qucs/python/parse_result_example.py index ba48b376..0f2f0e79 100644 --- a/qucs/python/parse_result_example.py +++ b/qucs/python/parse_result_example.py @@ -9,20 +9,20 @@ # create the dat file to load with: # qucsator < rc_ac_sweep.net > rc_ac_sweep.dat -data = QucsDataset('rc_ac_sweep.dat') +data = QucsDataset("rc_ac_sweep.dat") data.variables() -x = data.results('acfrequency') -y = np.abs(data.results('out.v')) -c = data.results('Cx') +x = data.results("acfrequency") +y = np.abs(data.results("out.v")) +c = data.results("Cx") -plt.loglog(x, y[0, :], '-rx') -plt.loglog(x, y[1, :], '-b.') -plt.loglog(x, y[4, :], '-go') +plt.loglog(x, y[0, :], "-rx") +plt.loglog(x, y[1, :], "-b.") +plt.loglog(x, y[4, :], "-go") -plt.legend(['Cx=' + str(c[0]), 'Cx=' + str(c[1]), 'Cx=' + str(c[4])]) +plt.legend(["Cx=" + str(c[0]), "Cx=" + str(c[1]), "Cx=" + str(c[4])]) -plt.xlabel('acfrequency') -plt.ylabel('abs(out.v)') +plt.xlabel("acfrequency") +plt.ylabel("abs(out.v)") plt.grid() plt.show() diff --git a/qucs/python/parse_result_example_2.py b/qucs/python/parse_result_example_2.py index 61885f7a..1f0ec2fc 100644 --- a/qucs/python/parse_result_example_2.py +++ b/qucs/python/parse_result_example_2.py @@ -9,17 +9,17 @@ # to create the dat.ngspice file to load with: # load the file rc_tran_ac.sch into QUCS-S and run the simulation -data = QucsDataset('rc_tran_ac.dat.ngspice') +data = QucsDataset("rc_tran_ac.dat.ngspice") data.variables() -fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(15, 5)) +fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(15, 5)) -axs[0].semilogx(data.results('frequency'), data.results('ac.v(vn3)'), '-ro') -axs[1].plot(data.results('time'), data.results('tran.v(vn3)'), '--b') -axs[0].set_xlabel('Time (s)') -axs[0].set_ylabel('Vn3 (V)') -axs[1].set_xlabel('frequency (Hz)') -axs[1].set_ylabel('Vn3 (V)') +axs[0].semilogx(data.results("frequency"), data.results("ac.v(vn3)"), "-ro") +axs[1].plot(data.results("time"), data.results("tran.v(vn3)"), "--b") +axs[0].set_xlabel("Time (s)") +axs[0].set_ylabel("Vn3 (V)") +axs[1].set_xlabel("frequency (Hz)") +axs[1].set_ylabel("Vn3 (V)") axs[0].grid() axs[1].grid()