Source code for qpdk.simulation.vacask.raw

"""Reader for the SPICE raw files written by VACASK."""

from __future__ import annotations

import itertools
from dataclasses import dataclass
from pathlib import Path

import numpy as np

from .errors import VacaskError


[docs] @dataclass(frozen=True) class RawPlot: """One plot of a SPICE raw file. Attributes: title: Circuit title. plotname: Analysis description written by the simulator. names: Variable names, in column order. data: Array of shape ``(points, variables)``; complex for complex plots. """ title: str plotname: str names: tuple[str, ...] data: np.ndarray def __getitem__(self, name: str) -> np.ndarray: """Return the column of variable ``name``. Raises: KeyError: If the plot has no variable called ``name``. """ try: return self.data[:, self.names.index(name)] except ValueError: raise KeyError(f"{name!r} not in {self.names}") from None def __contains__(self, name: object) -> bool: """Whether the plot has a variable called ``name``.""" return name in self.names
[docs] def split(self, name: str) -> list[RawPlot]: """Split a swept plot into one plot per value of the sweep variable ``name``. A new group starts wherever the sweep column changes value. Returns: One plot per sweep point, in order. """ column = self[name] starts = [0, *(np.flatnonzero(column[1:] != column[:-1]) + 1), len(column)] return [ RawPlot(self.title, self.plotname, self.names, self.data[a:b]) for a, b in itertools.pairwise(starts) ]
[docs] class RawFile: """A SPICE raw file (binary or ASCII) as written by VACASK. The file may hold several plots back to back. Binary data is little-endian ``float64`` (``complex128`` for complex plots), stored point-major. Attributes: path: Path of the file. plots: The plots in file order. Raises: VacaskError: If the file is truncated or malformed. """ def __init__(self, path: str | Path) -> None: """Read and parse ``path``.""" self.path = Path(path) self.plots = self._parse(self.path.read_bytes()) def __len__(self) -> int: """Number of plots.""" return len(self.plots) def __getitem__(self, index: int) -> RawPlot: """Plot number ``index``.""" return self.plots[index] def _error(self, message: str) -> VacaskError: """Return an error that names this raw file.""" return VacaskError(f"{self.path}: {message}") @staticmethod def _parse_header(lines: list[str]) -> tuple[dict[str, str], list[str]]: """Parse the text header of one plot into its fields and variable names.""" header: dict[str, str] = {} names: list[str] = [] in_variables = False for line in lines: if in_variables and line[:1] in {"\t", " "}: names.append(line.split()[1]) continue key, _, value = line.partition(":") in_variables = key == "Variables" header[key] = value.strip() return header, names def _parse_ascii( self, text: str, npoints: int, nvars: int, dtype: type ) -> np.ndarray: """Parse the ``Values:`` section of an ASCII raw file.""" tokens = text.split() if len(tokens) < npoints * (nvars + 1): raise self._error("ASCII data is truncated") data = np.empty((npoints, nvars), dtype=dtype) pos = 0 try: # ruff: ignore[too-many-statements-in-try-clause] for point in range(npoints): pos += 1 # point index for var in range(nvars): token = tokens[pos] pos += 1 if dtype is complex: real, _, imag = token.partition(",") data[point, var] = complex(float(real), float(imag or 0.0)) else: data[point, var] = float(token) except ValueError as error: raise self._error(f"malformed ASCII value {tokens[pos - 1]!r}") from error return data def _parse(self, raw: bytes) -> list[RawPlot]: """Split a raw file into its plots and parse each one.""" plots: list[RawPlot] = [] pos = 0 while pos < len(raw) and raw[pos:].strip(): markers = [ m for m in (raw.find(b"Binary:\n", pos), raw.find(b"Values:\n", pos)) if m >= 0 ] if not markers: raise self._error("no 'Binary:' or 'Values:' section") marker = min(markers) header, names = self._parse_header(raw[pos:marker].decode().splitlines()) try: npoints = int(header["No. Points"]) nvars = int(header["No. Variables"]) except (KeyError, ValueError) as error: raise self._error(f"bad header field {error}") from error if len(names) != nvars: raise self._error(f"header lists {len(names)} of {nvars} variables") is_complex = "complex" in header.get("Flags", "") start = marker + len(b"Binary:\n") if raw.startswith(b"Binary:\n", marker): dtype = np.dtype("<c16" if is_complex else "<f8") end = start + npoints * nvars * dtype.itemsize if end > len(raw): raise self._error("binary data is truncated") data = np.frombuffer(raw[start:end], dtype=dtype).reshape( npoints, nvars ) else: next_plot = raw.find(b"Title:", start) end = len(raw) if next_plot < 0 else next_plot data = self._parse_ascii( raw[start:end].decode(), npoints, nvars, complex if is_complex else float, ) plots.append( RawPlot( header.get("Title", ""), header.get("Plotname", ""), tuple(names), data, ) ) pos = end return plots