Utility helpers
Package
Module scubas.utils
Class RecursiveNamespace, GreatCircle
Method / Function fft, ifft, frexp102str, component_mappings, component_sign_mappings
Core dependency module
Utilities here are reused across cable, conductivity, and model workflows.
Changes in this module can impact multiple parts of SCUBAS.
scubas.utils provides the small supporting functions and classes used across
the codebase. The refactor introduced dataclass-friendly namespaces, more
robust FFT scaling, and guards against invalid input values.
Highlights:
RecursiveNamespace recursively converts mapping/list entries, which is
used by the cable/conductivity modules to normalise configuration dictionaries.
frexp102str now accepts zero and negative values, returning a formatted
scientific-notation string.
fft/ifft wrap NumPy’s real FFT operations with consistent scaling and
input validation.
GreatCircle exposes both great-circle and haversine distance utilities with
input checking.
Quick start
import numpy as np
from scubas.utils import RecursiveNamespace, fft, ifft, GreatCircle
cfg = RecursiveNamespace(alpha=1, nested={"beta": 2})
print(cfg.nested.beta)
t = np.linspace(0, 1, 128, endpoint=False)
signal = np.sin(2 * np.pi * 5 * t)
spectrum, freqs = fft(signal, dT=t[1] - t[0])
reconstructed = ifft(spectrum)
start = RecursiveNamespace(lat=0.0, lon=0.0)
end = RecursiveNamespace(lat=0.0, lon=90.0)
gc = GreatCircle(start, end)
print(gc.haversine())
API reference
scubas.utils.RecursiveNamespace
Bases: SimpleNamespace
Nested namespace that recursively converts dictionaries and lists.
Source code in scubas/utils.py
| class RecursiveNamespace(SimpleNamespace):
"""
Nested namespace that recursively converts dictionaries and lists.
"""
def __init__(self, **kwargs: Any) -> None:
converted = {key: self._convert(value) for key, value in kwargs.items()}
super().__init__(**converted)
@classmethod
def _convert(cls, value: Any) -> Any:
if isinstance(value, Mapping):
return cls(**value)
if isinstance(value, list):
return [cls._convert(item) for item in value]
return value
|
scubas.utils.frexp102str(x, precision=2)
Represent x using mantissa-exponent scientific notation.
Source code in scubas/utils.py
| def frexp102str(x: float, precision: int = 2) -> str:
"""
Represent ``x`` using mantissa-exponent scientific notation.
"""
if x == 0:
return "0"
sign = "-" if x < 0 else ""
value = abs(x)
exponent = int(math.floor(math.log10(value)))
mantissa = value / (10**exponent)
fmt = f"{{:{'.' + str(precision) if precision >= 0 else ''}f}}"
return f"{sign}{fmt.format(mantissa)}$\\times 10^{{{exponent}}}$"
|
scubas.utils.fft(X, dT, remove_zero_frequency=True)
Forward FFT with consistent scaling for real-valued time series.
Source code in scubas/utils.py
| def fft(
X: Sequence[float],
dT: float,
remove_zero_frequency: bool = True,
) -> Tuple[np.ndarray, np.ndarray]:
"""
Forward FFT with consistent scaling for real-valued time series.
"""
data = np.asarray(X, dtype=float)
if data.size == 0:
raise ValueError("FFT requires at least one sample.")
if dT <= 0:
raise ValueError("Sampling interval 'dT' must be positive.")
spectrum = 2.0 / data.size * np.fft.rfft(data)
freqs = np.fft.rfftfreq(data.size, d=dT)
if remove_zero_frequency and freqs.size > 1:
freqs[0] = freqs[1]
return spectrum, freqs
|
scubas.utils.ifft(Y)
Inverse FFT matching the scaling used in :func:fft.
Source code in scubas/utils.py
| def ifft(Y: Sequence[complex]) -> np.ndarray:
"""
Inverse FFT matching the scaling used in :func:`fft`.
"""
spectrum = np.asarray(Y, dtype=complex)
if spectrum.size == 0:
raise ValueError("IFFT requires at least one sample.")
return np.fft.irfft(spectrum) * 2 * spectrum.size
|
scubas.utils.component_mappings(field='B2E', comp='X')
Map field component names between B-field and E-field conventions.
Source code in scubas/utils.py
| def component_mappings(field: str = "B2E", comp: str = "X") -> str:
"""
Map field component names between B-field and E-field conventions.
"""
mappings: Mapping[str, Mapping[str, str]] = {
"B2E": {"X": "Y", "Y": "X"},
}
try:
return mappings[field][comp]
except KeyError as exc:
raise KeyError(
f"Unsupported component mapping for field '{field}' and component '{comp}'."
) from exc
|
scubas.utils.component_sign_mappings(fromto='BxEy')
Provide sign adjustments for specific component transformations.
Source code in scubas/utils.py
| def component_sign_mappings(fromto: str = "BxEy") -> float:
"""
Provide sign adjustments for specific component transformations.
"""
mappings: Mapping[str, float] = {
"BxEy": -1.0,
"ByEx": 1.0,
}
try:
return mappings[fromto]
except KeyError as exc:
raise KeyError(f"Unsupported component sign mapping '{fromto}'.") from exc
|
scubas.utils.GreatCircle
dataclass
Compute great-circle and haversine distances (km) between locations.
Source code in scubas/utils.py
| @dataclass(frozen=True)
class GreatCircle:
"""
Compute great-circle and haversine distances (km) between locations.
"""
initial: Any
final: Any
Re: float = 6371.0
@staticmethod
def _has_location_fields(loc: Any) -> bool:
return hasattr(loc, "lat") and hasattr(loc, "lon")
def _to_radians(self, loc: Any) -> Tuple[float, float]:
if not self._has_location_fields(loc):
raise ValueError("Location objects must expose 'lat' and 'lon' attributes.")
return math.radians(float(loc.lat)), math.radians(float(loc.lon))
def great_circle(
self, initial: Optional[Any] = None, final: Optional[Any] = None
) -> float:
"""
Great-circle distance assuming a spherical Earth.
"""
start = initial if initial is not None else self.initial
end = final if final is not None else self.final
lat1, lon1 = self._to_radians(start)
lat2, lon2 = self._to_radians(end)
cosine_argument = math.sin(lat1) * math.sin(lat2) + math.cos(lat1) * math.cos(
lat2
) * math.cos(lon1 - lon2)
cosine_argument = max(-1.0, min(1.0, cosine_argument))
return self.Re * math.acos(cosine_argument)
def haversine(
self, initial: Optional[Any] = None, final: Optional[Any] = None
) -> float:
"""
Haversine distance offering improved numerical stability.
"""
start = initial if initial is not None else self.initial
end = final if final is not None else self.final
lat1, lon1 = self._to_radians(start)
lat2, lon2 = self._to_radians(end)
dlon = lon2 - lon1
dlat = lat2 - lat1
a = (
math.sin(dlat / 2) ** 2
+ math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
)
a = min(1.0, max(0.0, a))
return 2 * self.Re * math.asin(math.sqrt(a))
|
great_circle(initial=None, final=None)
Great-circle distance assuming a spherical Earth.
Source code in scubas/utils.py
| def great_circle(
self, initial: Optional[Any] = None, final: Optional[Any] = None
) -> float:
"""
Great-circle distance assuming a spherical Earth.
"""
start = initial if initial is not None else self.initial
end = final if final is not None else self.final
lat1, lon1 = self._to_radians(start)
lat2, lon2 = self._to_radians(end)
cosine_argument = math.sin(lat1) * math.sin(lat2) + math.cos(lat1) * math.cos(
lat2
) * math.cos(lon1 - lon2)
cosine_argument = max(-1.0, min(1.0, cosine_argument))
return self.Re * math.acos(cosine_argument)
|
haversine(initial=None, final=None)
Haversine distance offering improved numerical stability.
Source code in scubas/utils.py
| def haversine(
self, initial: Optional[Any] = None, final: Optional[Any] = None
) -> float:
"""
Haversine distance offering improved numerical stability.
"""
start = initial if initial is not None else self.initial
end = final if final is not None else self.final
lat1, lon1 = self._to_radians(start)
lat2, lon2 = self._to_radians(end)
dlon = lon2 - lon1
dlat = lat2 - lat1
a = (
math.sin(dlat / 2) ** 2
+ math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
)
a = min(1.0, max(0.0, a))
return 2 * self.Re * math.asin(math.sqrt(a))
|