Skip to content

API Overview

The package currently centers on three public objects.

TimeSeries

TimeSeries represents one-dimensional sampled time-domain data and the sample spacing dt.

Typical operations:

  • inspect times
  • transform to FrequencySeries
  • transform to WDM
  • call plot()

FrequencySeries

FrequencySeries represents one-dimensional FFT-domain data and the frequency spacing df.

Typical operations:

  • inspect freqs
  • convert back to TimeSeries
  • transform to WDM
  • call plot()

WDM

WDM stores the packed coefficient matrix and the transform metadata:

  • dt
  • window parameter a
  • auxiliary parameter d
  • backend

Typical operations:

  • WDM.from_time_series(...)
  • TimeSeries.to_wdm(...)
  • FrequencySeries.to_wdm(...)
  • to_time_series()
  • to_frequency_series()
  • plot()

Backend model

The backend system is intentionally small right now. A backend provides:

  • an array namespace
  • an FFT namespace
  • asarray(...)

That is enough for the current NumPy implementation and sets up a clean insertion point for JAX and CuPy later.

Live API Reference

The sections below are generated from live docstrings with mkdocstrings, so signatures stay aligned with the implementation.

Core Objects Reference

TimeSeries

Public import: from wdm_transform import TimeSeries

One-dimensional sampled time-domain data.

Parameters

data : array_like Sample values on a uniform time grid. dt : float Time spacing between adjacent samples. backend : Backend Array backend used for computation.

Source code in src/wdm_transform/datatypes/series.py
@dataclass(frozen=True)
class TimeSeries:
    """One-dimensional sampled time-domain data.

    Parameters
    ----------
    data : array_like
        Sample values on a uniform time grid.
    dt : float
        Time spacing between adjacent samples.
    backend : Backend
        Array backend used for computation.
    """

    data: Any
    dt: float
    backend: Backend = field(default_factory=get_backend)

    def __post_init__(self) -> None:
        backend = get_backend(self.backend)
        data = backend.asarray(self.data)

        if data.ndim != 1:
            raise ValueError("TimeSeries data must be one-dimensional.")
        if self.dt <= 0:
            raise ValueError("dt must be positive.")

        object.__setattr__(self, "backend", backend)
        object.__setattr__(self, "data", data)

    def __repr__(self) -> str:
        return (
            f"TimeSeries(n={self.n}, dt={self.dt}, df={self.df}, "
            f"fs={self.fs}, nyquist={self.nyquist}, duration={self.duration})"
        )

    @property
    def n(self) -> int:
        """Number of samples."""
        return int(self.data.shape[0])

    @property
    def df(self) -> float:
        """Fourier frequency spacing implied by the sample cadence."""
        return 1.0 / (self.n * self.dt)

    @property
    def fs(self) -> float:
        """Sampling frequency of the underlying time-domain grid."""
        return 1.0 / self.dt

    @property
    def nyquist(self) -> float:
        """Nyquist frequency of the underlying sampled signal."""
        return 0.5 * self.fs

    @property
    def duration(self) -> float:
        """Total signal duration ``n * dt``.

        This follows the discrete-Fourier convention used throughout the
        package, not ``times[-1] - times[0]``.
        """
        return self.n * self.dt

    @property
    def times(self) -> Any:
        """Sample-time grid ``arange(n) * dt``."""
        return self.backend.xp.arange(self.n) * self.dt

    def to_frequency_series(self) -> "FrequencySeries":
        """Return the discrete Fourier transform of this series.

        The output keeps the same backend and uses the Fourier spacing implied
        by ``self.dt``.
        """
        transformed = self.backend.fft.fft(self.data)
        return FrequencySeries(transformed, df=self.df, backend=self.backend)

    def to_wdm(
        self,
        *,
        nt: int,
        a: float = 1.0 / 3.0,
        d: float = 1.0,
        backend: str | Backend | None = None,
    ) -> "WDM":
        """Compute the WDM transform of this time series."""
        from .wdm import WDM

        return WDM.from_time_series(self, nt=nt, a=a, d=d, backend=backend)

    def plot(self, **kwargs: Any) -> tuple[Any, Any]:
        """Plot the time-domain samples using the shared plotting helper."""
        from ..plotting import plot_time_series

        return plot_time_series(self, **kwargs)

n property

n: int

Number of samples.

df property

df: float

Fourier frequency spacing implied by the sample cadence.

fs property

fs: float

Sampling frequency of the underlying time-domain grid.

nyquist property

nyquist: float

Nyquist frequency of the underlying sampled signal.

duration property

duration: float

Total signal duration n * dt.

This follows the discrete-Fourier convention used throughout the package, not times[-1] - times[0].

times property

times: Any

Sample-time grid arange(n) * dt.

to_frequency_series

to_frequency_series() -> 'FrequencySeries'

Return the discrete Fourier transform of this series.

The output keeps the same backend and uses the Fourier spacing implied by self.dt.

Source code in src/wdm_transform/datatypes/series.py
def to_frequency_series(self) -> "FrequencySeries":
    """Return the discrete Fourier transform of this series.

    The output keeps the same backend and uses the Fourier spacing implied
    by ``self.dt``.
    """
    transformed = self.backend.fft.fft(self.data)
    return FrequencySeries(transformed, df=self.df, backend=self.backend)

to_wdm

to_wdm(*, nt: int, a: float = 1.0 / 3.0, d: float = 1.0, backend: str | Backend | None = None) -> 'WDM'

Compute the WDM transform of this time series.

Source code in src/wdm_transform/datatypes/series.py
def to_wdm(
    self,
    *,
    nt: int,
    a: float = 1.0 / 3.0,
    d: float = 1.0,
    backend: str | Backend | None = None,
) -> "WDM":
    """Compute the WDM transform of this time series."""
    from .wdm import WDM

    return WDM.from_time_series(self, nt=nt, a=a, d=d, backend=backend)

plot

plot(**kwargs: Any) -> tuple[Any, Any]

Plot the time-domain samples using the shared plotting helper.

Source code in src/wdm_transform/datatypes/series.py
def plot(self, **kwargs: Any) -> tuple[Any, Any]:
    """Plot the time-domain samples using the shared plotting helper."""
    from ..plotting import plot_time_series

    return plot_time_series(self, **kwargs)

FrequencySeries

Public import: from wdm_transform import FrequencySeries

One-dimensional FFT-domain data with spacing metadata.

Parameters

data : array_like Spectrum samples on the discrete Fourier grid. df : float Frequency spacing between adjacent Fourier bins. backend : Backend Array backend used for computation.

Source code in src/wdm_transform/datatypes/series.py
@dataclass(frozen=True)
class FrequencySeries:
    """One-dimensional FFT-domain data with spacing metadata.

    Parameters
    ----------
    data : array_like
        Spectrum samples on the discrete Fourier grid.
    df : float
        Frequency spacing between adjacent Fourier bins.
    backend : Backend
        Array backend used for computation.
    """

    data: Any
    df: float
    backend: Backend = field(default_factory=get_backend)

    def __post_init__(self) -> None:
        backend = get_backend(self.backend)
        data = backend.asarray(self.data)

        if data.ndim != 1:
            raise ValueError("FrequencySeries data must be one-dimensional.")
        if self.df <= 0:
            raise ValueError("df must be positive.")

        object.__setattr__(self, "backend", backend)
        object.__setattr__(self, "data", data)

    def __repr__(self) -> str:
        return (
            f"FrequencySeries(n={self.n}, df={self.df}, dt={self.dt}, "
            f"fs={self.fs}, nyquist={self.nyquist}, duration={self.duration})"
        )

    @property
    def n(self) -> int:
        """Number of Fourier bins."""
        return int(self.data.shape[0])

    @property
    def dt(self) -> float:
        """Time spacing implied by the discrete Fourier grid."""
        return 1.0 / (self.n * self.df)

    @property
    def fs(self) -> float:
        """Sampling frequency of the underlying time-domain signal."""
        return 1.0 / self.dt

    @property
    def nyquist(self) -> float:
        """Nyquist frequency of the underlying sampled signal."""
        return 0.5 * self.fs

    @property
    def duration(self) -> float:
        """Total signal duration ``n * dt`` represented by the spectrum."""
        return self.n * self.dt

    @property
    def freqs(self) -> Any:
        """Discrete Fourier frequency grid."""
        return self.backend.fft.fftfreq(self.n, d=self.dt)

    def to_time_series(self, *, real: bool = False) -> TimeSeries:
        """Return the inverse discrete Fourier transform as a time series.

        Parameters
        ----------
        real : bool, default=False
            If ``True``, discard any residual imaginary part after the inverse
            FFT and return a real-valued series.
        """
        recovered = self.backend.fft.ifft(self.data)
        if real:
            recovered = self.backend.xp.real(recovered)
        return TimeSeries(recovered, dt=self.dt, backend=self.backend)

    def to_wdm(
        self,
        *,
        nt: int,
        a: float = 1.0 / 3.0,
        d: float = 1.0,
        backend: str | Backend | None = None,
    ) -> "WDM":
        """Compute the WDM transform of this frequency-domain series."""
        from .wdm import WDM

        return WDM.from_frequency_series(self, nt=nt, a=a, d=d, backend=backend)

    def plot(self, **kwargs: Any) -> tuple[Any, Any]:
        """Plot the spectrum using the shared plotting helper."""
        from ..plotting import plot_frequency_series

        return plot_frequency_series(self, **kwargs)

n property

n: int

Number of Fourier bins.

dt property

dt: float

Time spacing implied by the discrete Fourier grid.

fs property

fs: float

Sampling frequency of the underlying time-domain signal.

nyquist property

nyquist: float

Nyquist frequency of the underlying sampled signal.

duration property

duration: float

Total signal duration n * dt represented by the spectrum.

freqs property

freqs: Any

Discrete Fourier frequency grid.

to_time_series

to_time_series(*, real: bool = False) -> TimeSeries

Return the inverse discrete Fourier transform as a time series.

Parameters

real : bool, default=False If True, discard any residual imaginary part after the inverse FFT and return a real-valued series.

Source code in src/wdm_transform/datatypes/series.py
def to_time_series(self, *, real: bool = False) -> TimeSeries:
    """Return the inverse discrete Fourier transform as a time series.

    Parameters
    ----------
    real : bool, default=False
        If ``True``, discard any residual imaginary part after the inverse
        FFT and return a real-valued series.
    """
    recovered = self.backend.fft.ifft(self.data)
    if real:
        recovered = self.backend.xp.real(recovered)
    return TimeSeries(recovered, dt=self.dt, backend=self.backend)

to_wdm

to_wdm(*, nt: int, a: float = 1.0 / 3.0, d: float = 1.0, backend: str | Backend | None = None) -> 'WDM'

Compute the WDM transform of this frequency-domain series.

Source code in src/wdm_transform/datatypes/series.py
def to_wdm(
    self,
    *,
    nt: int,
    a: float = 1.0 / 3.0,
    d: float = 1.0,
    backend: str | Backend | None = None,
) -> "WDM":
    """Compute the WDM transform of this frequency-domain series."""
    from .wdm import WDM

    return WDM.from_frequency_series(self, nt=nt, a=a, d=d, backend=backend)

plot

plot(**kwargs: Any) -> tuple[Any, Any]

Plot the spectrum using the shared plotting helper.

Source code in src/wdm_transform/datatypes/series.py
def plot(self, **kwargs: Any) -> tuple[Any, Any]:
    """Plot the spectrum using the shared plotting helper."""
    from ..plotting import plot_frequency_series

    return plot_frequency_series(self, **kwargs)

WDM

Public import: from wdm_transform import WDM

Real-valued WDM coefficients together with transform metadata.

Parameters

coeffs : array, shape (nt, nf + 1) Real-valued coefficient matrix. Column m corresponds to frequency channel m (0 ≤ m ≤ nf). dt : float Sampling interval of the original time-domain signal. a : float Window roll-off parameter (default 1/3). d : float Reserved window parameter (default 1, currently unused). backend : Backend Array backend used for computation.

Source code in src/wdm_transform/datatypes/wdm.py
@dataclass(frozen=True)
class WDM:
    """Real-valued WDM coefficients together with transform metadata.

    Parameters
    ----------
    coeffs : array, shape (nt, nf + 1)
        Real-valued coefficient matrix.  Column m corresponds to
        frequency channel m (0 ≤ m ≤ nf).
    dt : float
        Sampling interval of the original time-domain signal.
    a : float
        Window roll-off parameter (default 1/3).
    d : float
        Reserved window parameter (default 1, currently unused).
    backend : Backend
        Array backend used for computation.
    """

    coeffs: Any
    dt: float
    a: float = 1.0 / 3.0
    d: float = 1.0
    backend: Backend = field(default_factory=get_backend)

    def __post_init__(self) -> None:
        backend = get_backend(self.backend)
        coeffs = backend.asarray(self.coeffs, dtype=backend.xp.float64)

        if coeffs.ndim != 2:
            raise ValueError("WDM coeffs must be a two-dimensional array.")
        if self.dt <= 0:
            raise ValueError("dt must be positive.")

        nt, ncols = (int(dim) for dim in coeffs.shape)
        nf = ncols - 1
        validate_transform_shape(nt, nf)
        validate_window_parameter(self.a)

        object.__setattr__(self, "backend", backend)
        object.__setattr__(self, "coeffs", coeffs)

    def __repr__(self) -> str:
        return (
            "WDM("
            f"nt={self.nt}, nf={self.nf}, n={self.n}, "
            f"dt={self.dt}, df={self.df}, fs={self.fs}, nyquist={self.nyquist}, "
            f"delta_t={self.delta_t}, delta_f={self.delta_f}, "
            f"duration={self.duration}, a={self.a}, d={self.d}"
            ")"
        )

    @classmethod
    def from_time_series(
        cls,
        series: TimeSeries,
        *,
        nt: int,
        a: float = 1.0 / 3.0,
        d: float = 1.0,
        backend: str | Backend | None = None,
    ) -> "WDM":
        """Compute the forward WDM transform of a time-domain signal.

        Parameters
        ----------
        series : TimeSeries
            Input signal.  Its length must be divisible by ``nt``.
        nt : int
            Number of WDM time bins (must be even).
        a : float
            Window roll-off parameter.
        d : float
            Reserved (unused).
        backend : str, Backend, or None
            Override backend; defaults to the series' backend.
        """
        resolved_backend = get_backend(backend or series.backend)
        if series.n % nt != 0:
            raise ValueError(
                f"TimeSeries length {series.n} is not divisible by nt={nt}."
            )
        nf = series.n // nt
        coeffs = from_time_to_wdm(
            series.data,
            nt=nt,
            nf=nf,
            a=a,
            d=d,
            dt=series.dt,
            backend=resolved_backend,
        )
        return cls(coeffs=coeffs, dt=series.dt, a=a, d=d, backend=resolved_backend)

    @classmethod
    def from_frequency_series(
        cls,
        series: FrequencySeries,
        *,
        nt: int,
        a: float = 1.0 / 3.0,
        d: float = 1.0,
        backend: str | Backend | None = None,
    ) -> "WDM":
        """Compute the forward WDM transform from a frequency-domain signal.

        Any non-Hermitian component of ``series`` is discarded so the result
        matches applying the WDM transform to ``real(ifft(series.data))``.

        Parameters
        ----------
        series : FrequencySeries
            Input spectrum.  Its length must be divisible by ``nt``.
        nt : int
            Number of WDM time bins (must be even).
        a : float
            Window roll-off parameter.
        d : float
            Reserved (unused).
        backend : str, Backend, or None
            Override backend; defaults to the series' backend.
        """
        resolved_backend = get_backend(backend or series.backend)
        if series.n % nt != 0:
            raise ValueError(
                f"FrequencySeries length {series.n} is not divisible by nt={nt}."
            )
        nf = series.n // nt
        coeffs = from_freq_to_wdm(
            series.data,
            nt=nt,
            nf=nf,
            a=a,
            d=d,
            dt=series.dt,
            backend=resolved_backend,
        )
        return cls(coeffs=coeffs, dt=series.dt, a=a, d=d, backend=resolved_backend)

    @property
    def nt(self) -> int:
        """Number of WDM time bins."""
        return int(self.coeffs.shape[0])

    @property
    def nf(self) -> int:
        """Number of interior frequency channels.

        The total number of frequency channels is ``nf + 1``
        (m = 0, 1, …, nf), so ``coeffs.shape[1] == nf + 1``.
        """
        return int(self.coeffs.shape[1]) - 1

    @property
    def shape(self) -> tuple[int, int]:
        """(nt, nf + 1) shape of the coefficient matrix."""
        return (self.nt, self.nf + 1)

    @property
    def n(self) -> int:
        """Total number of time-domain samples represented by this transform."""
        return self.nt * self.nf

    @property
    def df(self) -> float:
        """Fourier-bin spacing of the underlying original signal.

        This is the discrete-Fourier spacing implied by the original sample
        cadence. For the WDM frequency-grid spacing, use :attr:`delta_f`.
        """
        return 1.0 / (self.nt * self.nf * self.dt)

    @property
    def fs(self) -> float:
        """Sampling frequency of the underlying original time series."""
        return 1.0 / self.dt

    @property
    def nyquist(self) -> float:
        """Nyquist frequency of the underlying original signal.

        This is also the frequency of the highest WDM channel,
        ``freq_grid[-1]``.
        """
        return 0.5 * self.fs

    @property
    def delta_t(self) -> float:
        """Spacing of the WDM time grid.

        Each WDM time bin spans ``nf * dt`` in the original sampling.
        For the underlying original sample spacing, use :attr:`dt`.
        """
        return self.nf * self.dt

    @property
    def delta_f(self) -> float:
        """Spacing of the WDM frequency grid.

        This is the spacing between adjacent WDM channels. For the underlying
        Fourier-bin spacing of the original signal, use :attr:`df`.
        """
        return 1.0 / (2.0 * self.delta_t)

    @property
    def duration(self) -> float:
        """Total signal duration ``nt * delta_t`` represented by this transform.

        Equivalently, this is ``nt * nf * dt``. This convention matches the
        transform construction and is not the same as ``time_grid[-1] - time_grid[0]``.
        """
        return self.nt * self.delta_t

    @property
    def time_grid(self) -> Any:
        """WDM time-grid coordinates ``arange(nt) * delta_t``."""
        return self.backend.xp.arange(self.nt) * self.delta_t

    @property
    def freq_grid(self) -> Any:
        """WDM frequency-grid coordinates ``arange(nf + 1) * delta_f``."""
        return self.backend.xp.arange(self.nf + 1) * self.delta_f

    @property
    def dc_channel(self) -> Any:
        """DC edge-channel coefficients (m = 0)."""
        return self.coeffs[:, 0]

    @property
    def nyquist_channel(self) -> Any:
        """Nyquist edge-channel coefficients (m = nf)."""
        return self.coeffs[:, self.nf]

    def to_time_series(self) -> TimeSeries:
        """Reconstruct the time-domain signal via the inverse WDM transform."""
        recovered = from_wdm_to_time(
            self.coeffs,
            a=self.a,
            d=self.d,
            dt=self.dt,
            backend=self.backend,
        )
        return TimeSeries(recovered, dt=self.dt, backend=self.backend)

    def to_frequency_series(self) -> FrequencySeries:
        """Reconstruct the frequency-domain signal from the Gabor atom expansion."""
        recovered = from_wdm_to_freq(
            self.coeffs,
            dt=self.dt,
            a=self.a,
            d=self.d,
            backend=self.backend,
        )
        return FrequencySeries(recovered, df=self.df, backend=self.backend)

    def plot(self, **kwargs: Any) -> tuple[Any, Any]:
        """Plot the WDM coefficient grid using the shared plotting helper."""
        from ..plotting import plot_wdm_grid

        return plot_wdm_grid(self, **kwargs)

from_time_series classmethod

from_time_series(series: TimeSeries, *, nt: int, a: float = 1.0 / 3.0, d: float = 1.0, backend: str | Backend | None = None) -> 'WDM'

Compute the forward WDM transform of a time-domain signal.

Parameters

series : TimeSeries Input signal. Its length must be divisible by nt. nt : int Number of WDM time bins (must be even). a : float Window roll-off parameter. d : float Reserved (unused). backend : str, Backend, or None Override backend; defaults to the series' backend.

Source code in src/wdm_transform/datatypes/wdm.py
@classmethod
def from_time_series(
    cls,
    series: TimeSeries,
    *,
    nt: int,
    a: float = 1.0 / 3.0,
    d: float = 1.0,
    backend: str | Backend | None = None,
) -> "WDM":
    """Compute the forward WDM transform of a time-domain signal.

    Parameters
    ----------
    series : TimeSeries
        Input signal.  Its length must be divisible by ``nt``.
    nt : int
        Number of WDM time bins (must be even).
    a : float
        Window roll-off parameter.
    d : float
        Reserved (unused).
    backend : str, Backend, or None
        Override backend; defaults to the series' backend.
    """
    resolved_backend = get_backend(backend or series.backend)
    if series.n % nt != 0:
        raise ValueError(
            f"TimeSeries length {series.n} is not divisible by nt={nt}."
        )
    nf = series.n // nt
    coeffs = from_time_to_wdm(
        series.data,
        nt=nt,
        nf=nf,
        a=a,
        d=d,
        dt=series.dt,
        backend=resolved_backend,
    )
    return cls(coeffs=coeffs, dt=series.dt, a=a, d=d, backend=resolved_backend)

from_frequency_series classmethod

from_frequency_series(series: FrequencySeries, *, nt: int, a: float = 1.0 / 3.0, d: float = 1.0, backend: str | Backend | None = None) -> 'WDM'

Compute the forward WDM transform from a frequency-domain signal.

Any non-Hermitian component of series is discarded so the result matches applying the WDM transform to real(ifft(series.data)).

Parameters

series : FrequencySeries Input spectrum. Its length must be divisible by nt. nt : int Number of WDM time bins (must be even). a : float Window roll-off parameter. d : float Reserved (unused). backend : str, Backend, or None Override backend; defaults to the series' backend.

Source code in src/wdm_transform/datatypes/wdm.py
@classmethod
def from_frequency_series(
    cls,
    series: FrequencySeries,
    *,
    nt: int,
    a: float = 1.0 / 3.0,
    d: float = 1.0,
    backend: str | Backend | None = None,
) -> "WDM":
    """Compute the forward WDM transform from a frequency-domain signal.

    Any non-Hermitian component of ``series`` is discarded so the result
    matches applying the WDM transform to ``real(ifft(series.data))``.

    Parameters
    ----------
    series : FrequencySeries
        Input spectrum.  Its length must be divisible by ``nt``.
    nt : int
        Number of WDM time bins (must be even).
    a : float
        Window roll-off parameter.
    d : float
        Reserved (unused).
    backend : str, Backend, or None
        Override backend; defaults to the series' backend.
    """
    resolved_backend = get_backend(backend or series.backend)
    if series.n % nt != 0:
        raise ValueError(
            f"FrequencySeries length {series.n} is not divisible by nt={nt}."
        )
    nf = series.n // nt
    coeffs = from_freq_to_wdm(
        series.data,
        nt=nt,
        nf=nf,
        a=a,
        d=d,
        dt=series.dt,
        backend=resolved_backend,
    )
    return cls(coeffs=coeffs, dt=series.dt, a=a, d=d, backend=resolved_backend)

nt property

nt: int

Number of WDM time bins.

nf property

nf: int

Number of interior frequency channels.

The total number of frequency channels is nf + 1 (m = 0, 1, …, nf), so coeffs.shape[1] == nf + 1.

shape property

shape: tuple[int, int]

(nt, nf + 1) shape of the coefficient matrix.

n property

n: int

Total number of time-domain samples represented by this transform.

df property

df: float

Fourier-bin spacing of the underlying original signal.

This is the discrete-Fourier spacing implied by the original sample cadence. For the WDM frequency-grid spacing, use :attr:delta_f.

fs property

fs: float

Sampling frequency of the underlying original time series.

nyquist property

nyquist: float

Nyquist frequency of the underlying original signal.

This is also the frequency of the highest WDM channel, freq_grid[-1].

delta_t property

delta_t: float

Spacing of the WDM time grid.

Each WDM time bin spans nf * dt in the original sampling. For the underlying original sample spacing, use :attr:dt.

delta_f property

delta_f: float

Spacing of the WDM frequency grid.

This is the spacing between adjacent WDM channels. For the underlying Fourier-bin spacing of the original signal, use :attr:df.

duration property

duration: float

Total signal duration nt * delta_t represented by this transform.

Equivalently, this is nt * nf * dt. This convention matches the transform construction and is not the same as time_grid[-1] - time_grid[0].

time_grid property

time_grid: Any

WDM time-grid coordinates arange(nt) * delta_t.

freq_grid property

freq_grid: Any

WDM frequency-grid coordinates arange(nf + 1) * delta_f.

dc_channel property

dc_channel: Any

DC edge-channel coefficients (m = 0).

nyquist_channel property

nyquist_channel: Any

Nyquist edge-channel coefficients (m = nf).

to_time_series

to_time_series() -> TimeSeries

Reconstruct the time-domain signal via the inverse WDM transform.

Source code in src/wdm_transform/datatypes/wdm.py
def to_time_series(self) -> TimeSeries:
    """Reconstruct the time-domain signal via the inverse WDM transform."""
    recovered = from_wdm_to_time(
        self.coeffs,
        a=self.a,
        d=self.d,
        dt=self.dt,
        backend=self.backend,
    )
    return TimeSeries(recovered, dt=self.dt, backend=self.backend)

to_frequency_series

to_frequency_series() -> FrequencySeries

Reconstruct the frequency-domain signal from the Gabor atom expansion.

Source code in src/wdm_transform/datatypes/wdm.py
def to_frequency_series(self) -> FrequencySeries:
    """Reconstruct the frequency-domain signal from the Gabor atom expansion."""
    recovered = from_wdm_to_freq(
        self.coeffs,
        dt=self.dt,
        a=self.a,
        d=self.d,
        backend=self.backend,
    )
    return FrequencySeries(recovered, df=self.df, backend=self.backend)

plot

plot(**kwargs: Any) -> tuple[Any, Any]

Plot the WDM coefficient grid using the shared plotting helper.

Source code in src/wdm_transform/datatypes/wdm.py
def plot(self, **kwargs: Any) -> tuple[Any, Any]:
    """Plot the WDM coefficient grid using the shared plotting helper."""
    from ..plotting import plot_wdm_grid

    return plot_wdm_grid(self, **kwargs)

Backend Utilities Reference

Backend

Minimal backend wrapper around an array namespace and FFT namespace.

Source code in src/wdm_transform/backends/base.py
@dataclass(frozen=True)
class Backend:
    """Minimal backend wrapper around an array namespace and FFT namespace."""

    name: str
    xp: Any
    fft: Any

    def asarray(self, value: Any, dtype: Any | None = None) -> Any:
        """Convert a value into an array for this backend."""
        return self.xp.asarray(value, dtype=dtype)

asarray

asarray(value: Any, dtype: Any | None = None) -> Any

Convert a value into an array for this backend.

Source code in src/wdm_transform/backends/base.py
def asarray(self, value: Any, dtype: Any | None = None) -> Any:
    """Convert a value into an array for this backend."""
    return self.xp.asarray(value, dtype=dtype)

get_backend

Resolve a backend object from a name, instance, or environment default.

Source code in src/wdm_transform/backends/__init__.py
def get_backend(backend: str | Backend | None = None) -> Backend:
    """Resolve a backend object from a name, instance, or environment default."""
    if isinstance(backend, Backend):
        return backend
    if backend is None:
        backend = os.environ.get("WDM_BACKEND")
    if backend is None:
        return NUMPY_BACKEND
    try:
        return _BACKENDS[backend]
    except KeyError as exc:
        try:
            return _load_builtin_backend(backend)
        except ValueError:
            available = ", ".join(
                sorted(set(_BACKENDS) | set(_BUILTIN_BACKEND_LOADERS))
            )
            raise ValueError(
                f"Unknown backend {backend!r}. Available backends: {available}."
            ) from exc

register_backend

Register a backend by name and return the resulting backend object.

Source code in src/wdm_transform/backends/__init__.py
def register_backend(name: str, xp: Any, fft: Any) -> Backend:
    """Register a backend by name and return the resulting backend object."""
    backend = Backend(name=name, xp=xp, fft=fft)
    _BACKENDS[name] = backend
    return backend

Plotting Helpers Reference

These helpers power the datatype .plot() methods and remain available as standalone functions.

plot_time_series

Plot a time-domain series against its sample times.

Source code in src/wdm_transform/plotting.py
def plot_time_series(
    series: TimeSeries,
    *,
    ax: Any | None = None,
    label: str | None = None,
    **kwargs: Any,
) -> tuple[Any, Any]:
    """Plot a time-domain series against its sample times."""
    plt = _get_pyplot()
    if ax is None:
        fig, axis = plt.subplots()
    else:
        fig, axis = ax.figure, ax
    times = _to_numpy(series.times)
    axis.plot(times, _to_numpy(series.data), label=label, **kwargs)
    axis.set_ylabel("Amplitude")
    _fmt_time_axis(times, axis)
    return fig, axis

plot_frequency_series

Plot a frequency-domain series.

By default, only non-negative frequencies are shown.

Source code in src/wdm_transform/plotting.py
def plot_frequency_series(
    series: FrequencySeries,
    *,
    ax: Any | None = None,
    magnitude: bool = True,
    label: str | None = None,
    positive_only: bool = True,
    **kwargs: Any,
) -> tuple[Any, Any]:
    """Plot a frequency-domain series.

    By default, only non-negative frequencies are shown.
    """
    plt = _get_pyplot()
    if ax is None:
        fig, axis = plt.subplots()
    else:
        fig, axis = ax.figure, ax

    freqs = _to_numpy(series.freqs)
    data = _to_numpy(series.data)
    if positive_only:
        mask = freqs >= 0.0
        freqs = freqs[mask]
        data = data[mask]

    values = np.abs(data) if magnitude else data
    axis.plot(freqs, values, label=label, **kwargs)
    axis.set_xlabel("Frequency [Hz]")
    axis.set_ylabel("Magnitude" if magnitude else "Value")
    if positive_only and len(freqs) > 0:
        axis.set_xlim(0.0, float(freqs[-1]))
    return fig, axis

plot_periodogram

Plot the squared spectrum magnitude on log-log axes.

Source code in src/wdm_transform/plotting.py
def plot_periodogram(
    series: FrequencySeries,
    *,
    ax: Any | None = None,
    positive_only: bool = True,
    **kwargs: Any,
) -> tuple[Any, Any]:
    """Plot the squared spectrum magnitude on log-log axes."""
    plt = _get_pyplot()
    if ax is None:
        fig, axis = plt.subplots()
    else:
        fig, axis = ax.figure, ax

    freqs = _to_numpy(series.freqs)
    data = _to_numpy(series.data)
    if positive_only:
        mask = freqs > 0.0
        freqs = freqs[mask]
        data = data[mask]

    axis.loglog(freqs, np.abs(data) ** 2, **kwargs)
    axis.set_xlabel("Frequency [Hz]")
    axis.set_ylabel("Periodogram")
    return fig, axis

plot_spectrogram

Compute and plot a scipy spectrogram for a time-domain series.

Source code in src/wdm_transform/plotting.py
def plot_spectrogram(
    series: TimeSeries,
    *,
    ax: Any | None = None,
    spec_kwargs: dict[str, Any] | None = None,
    plot_kwargs: dict[str, Any] | None = None,
) -> tuple[Any, Any]:
    """Compute and plot a scipy spectrogram for a time-domain series."""
    spectrogram = _require_scipy()
    plt = _get_pyplot()
    if ax is None:
        fig, axis = plt.subplots()
    else:
        fig, axis = ax.figure, ax

    spec_kwargs = {} if spec_kwargs is None else dict(spec_kwargs)
    plot_kwargs = {} if plot_kwargs is None else dict(plot_kwargs)
    plot_kwargs.setdefault("cmap", "Reds")

    fs = 1.0 / series.dt
    freqs, times, sxx = spectrogram(_to_numpy(series.data), fs=fs, **spec_kwargs)
    mesh = axis.pcolormesh(times, freqs, sxx, shading="nearest", **plot_kwargs)
    _fmt_time_axis(times, axis)
    axis.set_ylabel("Frequency [Hz]")
    axis.set_ylim(top=fs / 2.0)
    colorbar = plt.colorbar(mesh, ax=axis)
    colorbar.set_label("Spectrogram Amplitude")
    return fig, axis

plot_wdm_grid

Render the WDM coefficient grid as an image over time and frequency.

Source code in src/wdm_transform/plotting.py
def plot_wdm_grid(
    wdm: WDM,
    *,
    ax: Any | None = None,
    zscale: str = "linear",
    freq_scale: str = "linear",
    absolute: bool = True,
    freq_range: tuple[float, float] | None = None,
    show_colorbar: bool = True,
    cmap: str | Any | None = None,
    norm: Any | None = None,
    cbar_label: str | None = None,
    nan_color: str = "black",
    detailed_axes: bool = False,
    show_gridinfo: bool = True,
    txtbox_kwargs: dict[str, Any] | None = None,
    whiten_by: np.ndarray | None = None,
    **kwargs: Any,
) -> tuple[Any, Any]:
    """Render the WDM coefficient grid as an image over time and frequency."""
    plt = _get_pyplot()
    from matplotlib.colors import LogNorm, TwoSlopeNorm

    if ax is None:
        fig, axis = plt.subplots()
    else:
        fig, axis = ax.figure, ax

    time_grid = _wdm_time_grid(wdm)
    freq_grid = _wdm_freq_grid(wdm)
    z = _wdm_unpacked_grid(wdm).T
    if whiten_by is not None:
        z = z / whiten_by
    if absolute:
        z = np.abs(z)
    else:
        z = np.real(z)

    if norm is None:
        try:
            if np.all(np.isnan(z)):
                raise ValueError("All WDM data is NaN.")
            if zscale == "log":
                positive = z[z > 0]
                norm = LogNorm(vmin=np.nanmin(positive), vmax=np.nanmax(positive))
            elif not absolute:
                norm = TwoSlopeNorm(
                    vmin=float(np.nanmin(z)),
                    vcenter=0.0,
                    vmax=float(np.nanmax(z)),
                )
        except Exception as exc:
            warnings.warn(
                f"Falling back to default linear normalization for WDM plot: {exc}",
                stacklevel=2,
            )
            norm = None

    if cmap is None:
        cmap = "viridis" if absolute else "bwr"
    cmap_obj = plt.get_cmap(cmap).copy()
    cmap_obj.set_bad(color=nan_color)

    image = axis.imshow(
        z,
        aspect="auto",
        extent=[time_grid[0], time_grid[-1], freq_grid[0], freq_grid[-1]],
        origin="lower",
        cmap=cmap_obj,
        norm=norm,
        interpolation="nearest",
        **kwargs,
    )

    if show_colorbar:
        colorbar = fig.colorbar(image, ax=axis)
        if cbar_label is None:
            cbar_label = "Absolute WDM Amplitude" if absolute else "WDM Amplitude"
        colorbar.set_label(cbar_label)

    axis.set_yscale(freq_scale)
    axis.set_ylabel("Frequency [Hz]")
    _fmt_time_axis(time_grid, axis)

    freq_range = freq_range or (float(freq_grid[0]), float(freq_grid[-1]))
    axis.set_ylim(freq_range)

    if detailed_axes:
        axis.set_xlabel(rf"Time bins [$\Delta T$={wdm.delta_t:.4g}s, Nt={wdm.nt}]")
        axis.set_ylabel(
            rf"Frequency bins [$\Delta F$={wdm.delta_f:.4g}Hz, Nf={wdm.nf + 1}]"
        )

    label = kwargs.get("label", "")
    info = f"{wdm.nf + 1}x{wdm.nt}" if show_gridinfo else ""
    text = f"{label}\n{info}" if label and info else (label or info)
    if text:
        txtbox_kwargs = {} if txtbox_kwargs is None else dict(txtbox_kwargs)
        txtbox_kwargs.setdefault("boxstyle", "round")
        txtbox_kwargs.setdefault("facecolor", "white")
        txtbox_kwargs.setdefault("alpha", 0.2)
        axis.text(
            0.05,
            0.95,
            text,
            transform=axis.transAxes,
            fontsize=12,
            verticalalignment="top",
            bbox=txtbox_kwargs,
        )

    fig.tight_layout()
    return fig, axis