"""Time-series reader (port of Matlab ``read_ts.m`` text path)."""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
[docs]
@dataclass(frozen=True)
class TsPosR:
"""Receiver depth vector for time-series output (meters, float64)."""
z: np.ndarray # float64 receiver depths
[docs]
@dataclass(frozen=True)
class TsPos:
"""Nested position struct matching Matlab ``Pos`` (here only receiver depths)."""
r: TsPosR
[docs]
@dataclass(frozen=True)
class TsReadResult:
"""ASCII time-series file: title, receiver depths, time vector, and RTS matrix."""
plot_title: str
pos: TsPos
tout: np.ndarray # float64, shape (nt,)
rts: np.ndarray # float64, shape (nt, nrz)
[docs]
def read_ts(data: str | bytes, *, encoding: str = "utf-8") -> TsReadResult:
"""Parse ASCII time-series data (Matlab ``read_ts.m`` non-``.mat`` path)."""
text = data.decode(encoding, errors="replace") if isinstance(data, bytes) else data
lines = text.splitlines()
if len(lines) < 2:
raise ValueError("time-series file too short")
plot_title = lines[0].strip()
toks = " ".join(lines[1:]).split()
if not toks:
raise ValueError("time-series file missing numeric body")
i = 0
try:
nrz = int(float(toks[i]))
i += 1
except (ValueError, IndexError) as e:
raise ValueError("time-series file missing receiver depth count") from e
if nrz < 0:
raise ValueError("time-series file has negative receiver depth count")
if len(toks) < i + nrz:
raise ValueError("time-series file missing receiver depths")
rz = np.array([float(t) for t in toks[i : i + nrz]], dtype=np.float64)
i += nrz
rem = toks[i:]
nrow = nrz + 1
if nrow <= 0:
raise ValueError("invalid row count in time-series file")
if len(rem) == 0:
temp = np.zeros((nrow, 0), dtype=np.float64)
else:
if len(rem) % nrow != 0:
raise ValueError(
f"time-series payload has {len(rem)} values; expected multiple of {nrow}"
)
vals = np.array([float(t) for t in rem], dtype=np.float64)
temp = vals.reshape((nrow, len(rem) // nrow), order="F")
tout = temp[0, :].copy()
rts = temp[1:, :].T.copy()
return TsReadResult(
plot_title=plot_title,
pos=TsPos(r=TsPosR(z=rz)),
tout=tout,
rts=rts,
)
[docs]
def read_ts_bytes(data: bytes, *, encoding: str = "utf-8") -> TsReadResult:
"""Like :func:`read_ts` for UTF-8 (or other) encoded bytes."""
return read_ts(data, encoding=encoding)