Fix binary parsers for UTF-16 headers and AC analysis

- Handle UTF-16 LE encoded .raw file headers (Windows/Wine output)
- Fix mixed-precision transient data: float64 time + float32 signals
- Fix AC analysis: all variables stored as complex128, not mixed
- Fix schematic parser losing components at SYMBOL boundaries
- Use proper atan2 for phase calculation, report magnitude in dB
This commit is contained in:
Ryan Malloy 2026-02-10 13:20:47 -07:00
parent 50953a4dea
commit a77874c972
2 changed files with 19 additions and 30 deletions

View File

@ -172,28 +172,14 @@ def _parse_binary_data(
- Complex AC: frequency (float64) + other vars as (float64, float64) pairs
"""
if is_complex:
# Complex data (AC analysis): freq as double + complex values
# freq (8 bytes) + (n_vars-1) * (real + imag) = 8 + (n-1)*16 bytes per point
bytes_per_point = 8 + (n_vars - 1) * 16
expected_bytes = bytes_per_point * points
# Complex data (AC analysis): ALL variables stored as complex128
# Each value is (real float64, imag float64) = 16 bytes
bytes_per_point = n_vars * 16
actual_points = len(data) // bytes_per_point
if len(data) >= expected_bytes:
result = np.zeros((n_vars, points), dtype=np.complex128)
offset = 0
for p in range(points):
# Frequency as double (real)
result[0, p] = struct.unpack("<d", data[offset:offset + 8])[0]
offset += 8
# Rest are complex (real, imag) pairs
for v in range(1, n_vars):
real = struct.unpack("<d", data[offset:offset + 8])[0]
imag = struct.unpack("<d", data[offset + 8:offset + 16])[0]
result[v, p] = complex(real, imag)
offset += 16
return result
# Read as flat complex128 array and reshape
flat = np.frombuffer(data[:actual_points * bytes_per_point], dtype=np.complex128)
return flat.reshape(actual_points, n_vars).T.copy()
# Real data - detect format from data size
# Mixed format: time (8 bytes) + other vars (4 bytes each)

View File

@ -10,6 +10,7 @@ This server provides tools for:
import json
from pathlib import Path
import numpy as np
from fastmcp import FastMCP
from . import __version__
@ -181,9 +182,9 @@ def get_waveform(
if x_axis is not None:
sampled = x_axis[::step]
# Convert complex to magnitude for frequency domain
if sampled.dtype == complex:
result["x_axis_data"] = [abs(x) for x in sampled]
# For frequency domain, take real part (imag is 0)
if np.iscomplexobj(sampled):
result["x_axis_data"] = sampled.real.tolist()
else:
result["x_axis_data"] = sampled.tolist()
result["returned_points"] = len(result["x_axis_data"])
@ -194,15 +195,17 @@ def get_waveform(
if data is not None:
sampled = data[::step]
# Handle complex data (AC analysis)
if sampled.dtype == complex:
if np.iscomplexobj(sampled):
import math
result["signals"][name] = {
"magnitude": [abs(x) for x in sampled],
"phase_degrees": [
(180 / 3.14159) * (x.imag / x.real if x.real != 0 else 0)
"magnitude_db": [
20 * math.log10(abs(x)) if abs(x) > 0 else -200
for x in sampled
],
"phase_degrees": [
math.degrees(math.atan2(x.imag, x.real))
for x in sampled
],
"real": [x.real for x in sampled],
"imag": [x.imag for x in sampled],
}
else:
result["signals"][name] = {"values": sampled.tolist()}