"""3D SSP grid (Bellhop3D ``SSPFIL``) reader (port of Matlab ``readssp3d.m``)."""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
from at_py.readwrite.ssp2d import _tokens
[docs]
@dataclass(frozen=True)
class SSP3DRead:
"""3D SSP cube ``cmat`` shape ``(Nz, Ny, Nx)`` (Matlab ``cmat(iz,:,:)`` is ``Ny``×``Nx``)."""
nx: int
ny: int
nz: int
segx_km: np.ndarray
segy_km: np.ndarray
segz_km: np.ndarray
cmat: np.ndarray
[docs]
def parse_ssp3d(text: str) -> SSP3DRead:
"""Parse a 3D SSPFIL (Matlab ``readssp3d``).
For each depth index ``iz``, one ``Nx``×``Ny`` block is read column-wise (Matlab
``fscanf(..., [Nx, Ny])``), then transposed into ``cmat(iz, :, :)``.
"""
tok = _tokens(text)
it = iter(tok)
try:
nx = int(next(it))
segx = np.array([float(next(it)) for _ in range(nx)], dtype=np.float64)
ny = int(next(it))
segy = np.array([float(next(it)) for _ in range(ny)], dtype=np.float64)
nz = int(next(it))
segz = np.array([float(next(it)) for _ in range(nz)], dtype=np.float64)
except StopIteration as e:
raise ValueError("SSPFIL (3D): truncated header (Nx/Segx/Ny/Segy/Nz/Segz)") from e
need = nx * ny * nz
rest: list[float] = []
for x in it:
rest.append(float(x))
if len(rest) < need:
raise ValueError(
f"SSPFIL (3D): need {need} sound-speed values, got {len(rest)} after header"
)
if len(rest) > need:
raise ValueError(
f"SSPFIL (3D): expected exactly {need} sound-speed values, got {len(rest)}"
)
arr = np.asarray(rest[:need], dtype=np.float64)
cmat = np.zeros((nz, ny, nx), dtype=np.float64)
for iz in range(nz):
sl = arr[iz * nx * ny : (iz + 1) * nx * ny]
m2 = sl.reshape((nx, ny), order="F").T
cmat[iz, :, :] = m2
return SSP3DRead(
nx=nx,
ny=ny,
nz=nz,
segx_km=segx,
segy_km=segy,
segz_km=segz,
cmat=cmat,
)
[docs]
def parse_ssp3d_bytes(data: bytes) -> SSP3DRead:
"""Parse 3D SSPFIL from UTF-8 text bytes."""
text = data.decode("utf-8", errors="replace")
return parse_ssp3d(text)
def _fmt_ssp_float(x: float) -> str:
return f"{x:.17g}"