Skip to content

Commit

Permalink
fix: add python 3.13 support
Browse files Browse the repository at this point in the history
  • Loading branch information
milssky committed Jan 5, 2025
1 parent ebc4a95 commit 387d720
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 125 deletions.
202 changes: 95 additions & 107 deletions qucs/python/parse_result.py
Original file line number Diff line number Diff line change
@@ -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("<Qucs Dataset"):
raise ValueError(f"Invalid QUCS-S dataset {name}.")
self.__qucs_dataset = f.readlines()
raise ValueError(f"Invalid QUCS-S dataset {self.file_path}.")
self._qucs_dataset = f.readlines()
except FileNotFoundError:
raise FileNotFoundError(f"QUCS-S dataset {name} not found.")
self._variables = {}
self.__data = self.__parse_qucs_result()

def __parse_qucs_result(self) -> 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('<indep'):
matched = re.search(r'<(\w+) (\S+) (\d+)>', 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("<indep"):
matched = re.search(r"<(?P<tag>\w+) (?P<var>\S+) (?P<points>\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('<dep'):
elif line.startswith("<dep"):
indep_count = 0
matched = re.search(r'<dep (\S+)', line)
name = matched.group(1)
# reserve a complex matrix to be on the safe side
data[name] = np.zeros(numpoints, dtype=np.complex128)
matched = re.search(r"<dep (?P<var>\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}")

20 changes: 10 additions & 10 deletions qucs/python/parse_result_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
16 changes: 8 additions & 8 deletions qucs/python/parse_result_example_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down

0 comments on commit 387d720

Please sign in to comment.