Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 195 additions & 36 deletions psf_utils/psf.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,80 @@ def __init__(self, filename, sep=':', use_cache=True, update_cache=True):
parser = ParsePSF()
try:
content = psf_filepath.read_text()
sections = parser.parse(filename, content)

# Hybrid approach: Try to separate header/types from values
# We look for the VALUE section
value_idx = content.find("\nVALUE")
if value_idx == -1:
value_idx = content.find("VALUE")

if value_idx != -1:
# We found VALUE section.
# Let's see if we can fast-read it.

# Check for GROUP in TRACE section before attempting fast read
# The TRACE section is before VALUE.
trace_idx = content.find("\nTRACE")
if trace_idx == -1:
trace_idx = content.find("TRACE")

has_group = False
if trace_idx != -1 and trace_idx < value_idx:
trace_section = content[trace_idx:value_idx]
if "GROUP" in trace_section:
has_group = True

fast_values = None
fast_names = None

if not has_group:
# Let's try to read the file as bytes for fast reading
with open(filename, 'rb') as f:
content_bytes = f.read()

# Try fast read of values
fast_values, fast_names = self._fast_read_values(content_bytes)

if fast_values is not None:
# Fast read successful!
# Now parse metadata.

# Truncate content for parser
prefix = content[:value_idx]
dummy_content = prefix + "\nVALUE\n\"dummy_var_for_fast_read\" 0.0\nEND"

sections = parser.parse(filename, dummy_content)

# Now we have metadata.
# We need to inject our fast values into 'sections'.
# sections = (meta, types, sweeps, traces, values)
meta, types, sweeps, traces, values = sections

# 'values' contains the dummy. We discard it.
values = {}

# Re-construct values dict
class Value(Info):
pass

for i, name in enumerate(fast_names):
# Extract column
col = fast_values[:, i]
# We wrap it in a Value object
# We flag it as 'fast_array' so __init__ knows
v_obj = Value(values=col, is_fast=True)
values[name] = v_obj

# Update sections
sections = (meta, types, sweeps, traces, values)

else:
# Fast read failed or skipped, fallback
sections = parser.parse(filename, content)
else:
# No VALUE section?
sections = parser.parse(filename, content)

except ParseError as e:
raise Error(str(e))
except OSError as e:
Expand All @@ -116,6 +189,7 @@ def __init__(self, filename, sep=':', use_cache=True, update_cache=True):
'\nUse `psf {0!s} {0!s}.ascii` to convert.'.format(psf_filepath),
)
)

meta, types, sweeps, traces, values = sections
self.meta = meta
self.types = types
Expand All @@ -126,7 +200,14 @@ def __init__(self, filename, sep=':', use_cache=True, update_cache=True):
if sweeps:
for sweep in sweeps:
n = sweep.name
sweep.abscissa = np.array([v[0] for v in values[n].values])
# Check if it's a fast value
val_obj = values[n]
if getattr(val_obj, 'is_fast', False):
# It's already a numpy array
sweep.abscissa = val_obj.values
else:
# Original logic
sweep.abscissa = np.array([v[0] for v in val_obj.values])

# process signals
# 1. convert to numpy and delete the original list
Expand All @@ -138,7 +219,11 @@ def __init__(self, filename, sep=':', use_cache=True, update_cache=True):
for trace in traces:
name = trace.name
type = types.get(trace.type, trace.type)
vals = values[name].values

val_obj = values[name]
vals = val_obj.values
is_fast = getattr(val_obj, 'is_fast', False)

if type == 'GROUP':
group = {k: types.get(v, v) for k, v in groups[name].items()}
prefix = ''
Expand All @@ -154,10 +239,18 @@ def __init__(self, filename, sep=':', use_cache=True, update_cache=True):
for i, v in enumerate(group.items()):
n, t = v
joined_name = prefix + n
if 'complex' in t.kind:
ordinate = np.array([complex(*get_value(v, i)) for v in vals])

if is_fast:
# If fast read, vals is already the numpy array for this signal
# And we assume fast read only handles simple scalar floats for now.
# So 'vals' IS the ordinate.
ordinate = vals
else:
ordinate = np.array([get_value(v, i) for v in vals])
if 'complex' in t.kind:
ordinate = np.array([complex(*get_value(v, i)) for v in vals])
else:
ordinate = np.array([get_value(v, i) for v in vals])

signal = Signal(
name = joined_name,
ordinate = ordinate,
Expand All @@ -177,45 +270,111 @@ def __init__(self, filename, sep=':', use_cache=True, update_cache=True):
else:
# no traces, this should be a DC op-point analysis dataset
for name, value in values.items():
assert len(value.values) == 1
type = types[value.type]
if type.struct:
for t, v in zip(type.struct.types.values(), value.values[0][0]):
n = f'{name}.{t.name}'
if 'float' in t.kind:
v = Quantity(v, unicode_units(t.units))
elif 'complex' in t.kind:
v = complex(v[0], v[1])
# For DC analysis, fast read likely failed or we didn't use it
is_fast = getattr(value, 'is_fast', False)
if is_fast:
v = value.values[0]
else:
assert len(value.values) == 1
type = types[value.type]
if type.struct:
for t, v in zip(type.struct.types.values(), value.values[0][0]):
n = f'{name}.{t.name}'
if 'float' in t.kind:
v = Quantity(v, unicode_units(t.units))
elif 'complex' in t.kind:
v = complex(v[0], v[1])
signal = Signal(
name = n,
ordinate = v,
type = t,
units = t.units,
meta = meta,
)
signals[n] = signal
else:
if 'float' in type.kind:
v = Quantity(value.values[0][0], unicode_units(type.units))
elif 'complex' in type.kind:
v = complex(value.values[0][0], value.values[0][1])
else:
v = value.values[0]

signal = Signal(
name = n,
name = name,
ordinate = v,
type = t,
units = t.units,
type = type,
access = type.name,
units = type.units,
meta = meta,
)
signals[n] = signal
else:
if 'float' in type.kind:
v = Quantity(value.values[0][0], unicode_units(type.units))
elif 'complex' in type.kind:
v = complex(value.values[0][0], value.values[0][1])
else:
v = value.values[0]

signal = Signal(
name = name,
ordinate = v,
type = type,
access = type.name,
units = type.units,
meta = meta,
)
signals[name] = signal
signals[name] = signal
self.signals = signals

if update_cache:
self._write_cache(cache_filepath)

def _fast_read_values(self, content_bytes):
"""
Attempts to read the VALUE section using fast numpy parsing.
Returns (values_array, names_list) if successful, or (None, None) if not.
"""
try:
# Find VALUE section
idx = content_bytes.find(b"\nVALUE\n")
if idx == -1:
idx = content_bytes.find(b"VALUE\n")
if idx == -1:
return None, None
start_offset = idx + 6
else:
start_offset = idx + 7

data_content = content_bytes[start_offset:]

# We need to stop at END if it exists
end_idx = data_content.rfind(b"\nEND")
if end_idx != -1:
data_content = data_content[:end_idx]

tokens = data_content.split()

if not tokens:
return None, None

# Identify signals
first_name_bytes = tokens[0]
cycle_len = 0
for i in range(2, len(tokens), 2):
if tokens[i] == first_name_bytes:
cycle_len = i // 2
break

if cycle_len == 0:
return None, None

names = [t.decode('utf-8').strip('"') for t in tokens[0:cycle_len*2:2]]

total_tokens = len(tokens)
num_rows = total_tokens // (2 * cycle_len)

if num_rows == 0:
return None, None

tokens = tokens[:num_rows * 2 * cycle_len]

try:
values = np.array(tokens[1::2], dtype=float)
except ValueError:
return None, None

data = values.reshape((num_rows, cycle_len))

return data, names

except Exception:
return None, None

def get_sweep(self, index=0):
"""
Get Sweep
Expand Down
Loading