Source code for pycsamt.emtf.validation
# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""Validation helpers for the format-neutral EMTF scientific model."""
from __future__ import annotations
from collections.abc import Iterable
import numpy as np
__all__ = [
"normalize_channels",
"normalize_periods",
"normalize_tf_data",
]
[docs]
def normalize_channels(channels: Iterable[str] | None) -> tuple[str, ...]:
"""Normalize channel names while preserving order."""
if channels is None:
return ()
out: list[str] = []
for value in channels:
name = str(value).strip()
if not name:
raise ValueError("channel names must be non-empty")
if name in out:
raise ValueError(f"duplicate channel name: {name!r}")
out.append(name)
return tuple(out)
[docs]
def normalize_periods(
periods,
*,
n_periods: int | None = None,
) -> np.ndarray | None:
"""Return a validated positive 1-D period vector."""
if periods is None:
return None
arr = np.asarray(periods, dtype=float)
if arr.ndim == 0:
arr = arr.reshape(1)
if arr.ndim != 1:
raise ValueError("periods must be a 1-D array")
if arr.size and (not np.all(np.isfinite(arr)) or np.any(arr <= 0.0)):
raise ValueError("periods must contain finite positive values")
if n_periods is not None and arr.size != int(n_periods):
raise ValueError(
"period count does not match transfer-function data: "
f"{arr.size} != {n_periods}"
)
return arr
[docs]
def normalize_tf_data(
data,
*,
n_output: int,
n_input: int,
) -> np.ndarray:
"""Normalize a TF payload to ``(n_period, n_output, n_input)``.
Empty channel lists represent scalar 1x1 products. A 1-D array is
therefore accepted only for a scalar response and is promoted to
``(n_period, 1, 1)``. A 2-D array matching one output/input matrix is
interpreted as a single period and promoted to 3-D.
"""
arr = np.asarray(data)
if arr.dtype.kind not in "biufc":
raise TypeError("transfer-function data must be numeric")
nout = max(1, int(n_output))
nin = max(1, int(n_input))
if arr.ndim == 0:
if (nout, nin) != (1, 1):
raise ValueError(
"scalar data are only valid for a scalar 1x1 response"
)
arr = arr.reshape(1, 1, 1)
elif arr.ndim == 1:
if (nout, nin) != (1, 1):
raise ValueError(
"1-D data require scalar input/output channel dimensions"
)
arr = arr[:, None, None]
elif arr.ndim == 2:
if arr.shape != (nout, nin):
raise ValueError(
"2-D data are interpreted as one TF matrix; expected "
f"{(nout, nin)}, got {arr.shape}"
)
arr = arr[None, ...]
elif arr.ndim == 3:
if arr.shape[1:] != (nout, nin):
raise ValueError(
"TF matrix shape does not match channel dimensions: "
f"expected (*, {nout}, {nin}), got {arr.shape}"
)
else:
raise ValueError(
"transfer-function data must be scalar, 1-D, 2-D, or 3-D"
)
return arr