import math import numpy as np import re from functools import total_ordering from ee.formatting import eng_str __all__ = [ "EeException", "EeVal", "read_ltspice_raw" ] class EeException(Exception): pass @total_ordering class EeVal(object): from decimal import Decimal units = ['F', 'Ohm', '\u2126', # Ohm symbol 'H'] exponents = { 'f': -15, 'p': -12, 'n': -9, 'u': -6, '\u00B5': -6, # The micro symbol 'm': -3, 'k': 3, 'M': 6, 'G': 9, 'T': 12, 'P': 15, 'E': 18, } r = re.compile( "([0-9]+\\.[0-9]+|\\.?[0-9]+|[0-9]+\\.?) *([" + "".join(exponents.keys()) + "]?) *(" + "|".join(units) + "?)") def __init__(self, s=None, value=None, exp=None, unit=None): # This is where I regret having a string in the API at once. if s: val = EeVal.parse(s) (self._value, self._exp, self._unit) = (val._value, val._exp, val._unit) else: if value is None or exp is None: raise EeException("Bad arguments, value and exp has to be set.") (self._value, self._exp, self._unit) = (value, exp, unit) @staticmethod def parse(s) -> "EeVal": m = EeVal.r.match(s) if not m: raise EeException("Could not parse value: " + str(s)) gs = m.groups() value = float(gs[0]) exp = gs[1].strip() exp = EeVal.exponents.get(exp) if len(exp) > 0 else 0 unit = gs[2] if value != 0 and value < 1: e = math.ceil(math.log10(value)) exp = exp + e value = value * math.pow(10, -e) return EeVal(None, value=float(value), exp=exp, unit=unit if len(unit) > 0 else None) @property def value(self): return self.__float__() @property def unit(self): return self._unit def set(self, value=None, exp=None, unit=None): return EeVal(None, value=value if value else self._value, exp=exp if exp else self._exp, unit=unit if unit else self._unit) def __hash__(self): return hash((self.__float__(), self._unit)) def __eq__(self, other): return math.isclose(self.__float__(), other.__float__()) and self._unit == other._unit def __lt__(self, other): # return ((self.__float__(), self._unit) < (other.__float__(), other._unit)) x = self.__float__() < other.__float__() if x != 0: return x if self._unit is None and other._unit is None: return 0 if self._unit is not None and other._unit is not None: return self._unit < other._unit return 1 if self.unit is not None else -1 def __str__(self): return eng_str(self.__float__(), self._unit) def __float__(self): return self._value * math.pow(10, self._exp) class LtSpiceRaw(object): def __init__(self, variables, values_first, values_rest): self.variables = variables self.values_first = values_first self.values_rest = values_rest def get_values(self, variable): return self.values[variable.idx] def get_variable(self, idx=None, expression=None): if idx is not None: return self.variables[idx] if expression is not None: v = [v for v in self.variables if v.expression == expression] if len(v) != 1: raise Exception('Unknown variable: ' + str(v)) return v[0] raise Exception('idx or expression must be given') def to_pandas(self): import pandas data = [] for (i, v) in enumerate(self.values_first): data.append(pandas.Series(v, dtype='float64')) data = {} for (i, v) in enumerate(self.values_first): data[self.variables[i].expression] = v return pandas.DataFrame(data=data) class LtSpiceVariable(object): def __init__(self, idx, expression, kind): self.idx = idx self.expression = expression self.kind = kind class LtSpiceReader(object): def __init__(self, f): self.f = f self.mode = 'header' self.headers = {} self.variables = [] self.values = [] def read_header(self, line): sep = line.find(':') key = line[0:sep] value = line[sep + 1:].strip() # print("key='{}', value='{}'".format(key, str(value))) if key == 'Binary': self.set_binary_mode() elif key == 'Variables': self.no_variables = int(self.headers['No. Variables']) self.mode = 'variables' else: self.headers[key] = value def read_variable(self, line): parts = line.split('\t') idx = int(parts[1]) expression = parts[2] kind = parts[3] self.variables.append(LtSpiceVariable(idx, expression, kind)) if len(self.variables) == self.no_variables: self.mode = 'header' def set_binary_mode(self): self.mode = 'binary' def read_binary(self): pos = self.f.tell() no_points = int(self.headers['No. Points']) no_variables = int(self.headers['No. Variables']) # print("no_points={}, no_variables={}".format(no_points, no_variables)) if True: self.values = np.zeros((no_variables, no_points), dtype='float32') for p in range(no_points): self.values[0][p] = np.fromfile(self.f, count=1, dtype='float64') row = np.fromfile(self.f, count=no_variables - 1, dtype='float32'); for v in range(0, len(row)): self.values[v + 1][p] = row[v] else: self.values = np.zeros((no_points, no_variables), dtype='float64') for p in range(no_points): self.values[p][0] = np.fromfile(self.f, count=1, dtype='float64') row = np.fromfile(self.f, count=no_variables - 1, dtype='float32'); for v in range(0, len(row)): self.values[p][v + 1] = row[v] def read(self): while True: line = self.f.readline(); if len(line) == 0: break self.f.read(1) # The data is utf16 encoded so read the extra null byte # if this wasn't the last line .readline() includes the newline if line[-1] == '\n' or line[-1] == 0x0a: line = line[:-1] # print("len(line)={}, line={}".format(len(line), str(line))) line = line.decode(encoding="utf-16") if self.mode == 'header': self.read_header(line) elif self.mode == 'variables': self.read_variable(line) # The binary data can't be read with readline() if self.mode == 'binary': # self.f.read() self.read_binary() break if len(self.variables) == 0: raise Exception("Something didn't quite work when parsing file, no variables found") return LtSpiceRaw(self.variables, self.values, []) def read_ltspice_raw(filename): with open(filename, mode="rb") as f: r = LtSpiceReader(f) return r.read()