Source code for dsf.hankel.hankel_transform_fftlog

"""FFTLog-basedHankel transforms for 1D radial statistics.

This module provides the ``HankelTransformFFTLog`` class, which converts
Fourier-space spectra into radial-space quantities using the FFTLog
algorithm. It implements 1D projected and spherical Hankel transforms
as a ``HankelTransform`` backend. It also includes two public functions that
perform the FFTLog-based Hankel transforms directly, without requiring a
``HankelTransform`` class instance.
"""

from __future__ import annotations

from collections.abc import Mapping
from typing import Any

import numpy as np
from scipy.fft import fhtoffset, ifht

from dsf.hankel.hankel_transform_base import HankelTransformBase
from dsf.utils.types import ArrayLike, FloatArray, SpectrumInput
from dsf.utils.validators import (
    as_1d_float_array,
    validate_hankel_1d_grid_spacing,
    validate_power_spectrum_inputs,
)


[docs] class HankelTransformFFTLog(HankelTransformBase): """Hankel transform using the FFTLog algorithm. This class provides methods for performing 1D Hankel transforms using the scipy FFTLog algorithm. """ def _evaluate_spectrum( self, spectrum: SpectrumInput, order: float | int = 0, radial_input: ArrayLike | None = None, taper: bool = False, taper_kwargs: Mapping[str, Any] | None = None, **kwargs, ) -> FloatArray: """Evaluate a spectrum on a supplied wavenumber grid. Args: spectrum: Tabulated spectrum values or callable spectrum. order: Bessel order whose grid should be used. radial_input: Radial grid for tabulated spectra (k or ell). taper: Whether to suppress low-k and high-k edge power. taper_kwargs: Optional settings for the spectrum taper. **kwargs: Extra arguments passed to callable spectra. Returns: Spectrum evaluated on the supplied wavenumber grid. """ if radial_input is None: raise ValueError("radial_input must be supplied.") if callable(spectrum): values = np.asarray(spectrum(radial_input, **kwargs), dtype=float) else: values = as_1d_float_array(spectrum, "spectrum", min_size=2) if taper: taper_kwargs = {} if taper_kwargs is None else taper_kwargs values = self.taper_spectrum( radial_input, values, **taper_kwargs, ) if np.any(~np.isfinite(values)): raise ValueError("Evaluated spectrum must contain only finite values.") return values
[docs] def projected_correlation( self, radial_input: ArrayLike | None = None, spectrum: SpectrumInput | None = None, order: float | int = 2, taper: bool = False, taper_kwargs: Mapping[str, Any] | None = None, use_offset: bool = False, **kwargs, ) -> tuple[FloatArray, FloatArray]: """Compute a projected radial statistic from one spectrum. Args: radial_input: Radial input grid for tabulated spectra. spectrum: Spectrum values or callable spectrum. order: Bessel order to use (default is 2 for tangential shear). taper: Whether to suppress low-k and high-k edge power. taper_kwargs: Optional settings for the spectrum taper. use_offset: Apply automatic offset to FFTLog to reduce ringing. **kwargs: Extra arguments passed to callable spectra. Returns: Radial grid, in inverse units of ``radial_input``, and the projected radial statistic. """ if radial_input is None: raise ValueError("radial_input must be supplied.") if spectrum is None: raise ValueError("spectrum must be supplied.") radial_input_arr = as_1d_float_array(radial_input, "radial_input", min_size=2) spectrum_eval = self._evaluate_spectrum( spectrum, order=order, radial_input=radial_input_arr, taper=taper, taper_kwargs=taper_kwargs, **kwargs, ) return hankel_projected( radial_input=radial_input_arr, spectrum=spectrum_eval, order=order, use_offset=use_offset, )
[docs] def spherical_correlation( self, k_pk: ArrayLike | None = None, pk: SpectrumInput | None = None, order: float | int = 0, taper: bool = False, taper_kwargs: Mapping[str, Any] | None = None, use_offset: bool = False, **kwargs, ) -> tuple[FloatArray, FloatArray]: """Compute a spherical radial statistic from one spectrum. Args: k_pk: Wavenumber grid for tabulated spectra (units of inverse distance). pk: Spectrum values or callable spectrum. order: Bessel order to use (default is 0 for Delta Sigma). taper: Whether to suppress low-k and high-k edge power. taper_kwargs: Optional settings for the spectrum taper. use_offset: Apply automatic offset to FFTLog to reduce ringing. **kwargs: Extra arguments passed to callable spectra. Returns: Radial grid and spherical radial statistic. The units of the radial grid are the inverse of the units of ``k``. """ if pk is None: raise ValueError("pk must be supplied.") k_pk_arr = as_1d_float_array(k_pk, "k_pk", min_size=2) pk_eval = self._evaluate_spectrum( pk, order=order, radial_input=k_pk_arr, taper=taper, taper_kwargs=taper_kwargs, **kwargs, ) return hankel_spherical(k=k_pk_arr, pk=pk_eval, order=order, use_offset=use_offset)
[docs] def hankel_projected( radial_input: FloatArray, spectrum: FloatArray, order: float | int = 2, use_offset: bool = False, ) -> tuple[FloatArray, FloatArray]: """Convert an angular power spectrum to a projected correlation using FFTLog. :math:`\\gamma_t(\\theta) = \\int \\frac{\\ell d\\ell}{2\\pi} C(\\ell )J_\\mu(\\ell \\theta)`. Args: radial_input: Radial input array (must be uniform in logspace). spectrum: Spectrum to transform. order: Bessel order to use (default is 2 for tangential shear). use_offset: Apply automatic offset to FFTLog to reduce ringing. Returns: Radial grid, in inverse units of ``radial_input``, and the projected radial statistic. """ radial_input_arr = validate_hankel_1d_grid_spacing(radial_input, "radial_input") spectrum_arr = as_1d_float_array(spectrum, "spectrum", min_size=2) validate_power_spectrum_inputs( radial_input_arr, spectrum_arr, k_name="radial_input", pk_name="spectrum", ) dln_radial_input = float(np.log(radial_input_arr[1] / radial_input_arr[0])) offset = fhtoffset(dln=dln_radial_input, mu=order) if use_offset else 0.0 transformed_power = ifht( spectrum_arr * radial_input_arr, dln=dln_radial_input, mu=order, offset=offset, ) theta = np.exp(offset) / radial_input_arr[::-1] prefactor = 1.0 / (2.0 * np.pi * theta) xi = np.asarray(prefactor * transformed_power, dtype=float) return theta, xi
[docs] def hankel_spherical( k: FloatArray, pk: FloatArray, order: float | int = 0, use_offset: bool = False, ) -> tuple[FloatArray, FloatArray]: """Convert power spectrum to 3D correlation function using FFTLog: :math:`\\xi(r) = \\int \\frac{k^2 dk}{2\\pi^2} P(k) j_\\ell(kr)`. Currently, only order 0 is implemented. Args: k: Wavenumber array (must be uniform in logspace). pk: Power spectrum to transform. order: Bessel order to use (default is 0 for Delta Sigma). use_offset: Apply automatic offset to FFTLog to reduce ringing. Returns: Radial grid and 3D correlation function. """ if order != 0: raise NotImplementedError( "Only order 0 is currently implemented for spherical Hankel transforms." ) k_arr = validate_hankel_1d_grid_spacing(k, "k") pk_arr = as_1d_float_array(pk, "pk", min_size=2) validate_power_spectrum_inputs(k_arr, pk_arr) dln_k = float(np.log(k_arr[1] / k_arr[0])) offset = fhtoffset(dln=dln_k, mu=0.5) if use_offset else 0.0 transformed_power = ifht( k_arr**1.5 * pk_arr, dln=dln_k, mu=0.5, offset=offset, ) r = np.exp(offset) / k_arr[::-1] prefactor = 1.0 / (2.0 * np.pi * r) ** 1.5 xi = np.asarray(prefactor * transformed_power, dtype=float) return r, xi