Source code for torch_harmonics.truncation

# coding=utf-8

# SPDX-FileCopyrightText: Copyright (c) 2026 The torch-harmonics Authors. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

import warnings
from typing import Optional, Tuple


def _truncate_lmax(nlat: int, grid: Optional[str] = "equiangular") -> int:
    """
    Truncate the maximum spherical harmonic degree based on the latitude grid. The maximum degree
    corresponds to the maximum degree of associated Legendre polynomials that can be square-integrated
    exactly.

    | Grid Type           | Includes Poles? | Exactness       | Heuristic ($L_{\text{max}}$) |
    | :---                | :---:           | :---:           | :---:                        |
    | Legendre-Gauss (GL) | No              | $2N - 1$        | $N - 1$                      |
    | Gauss-Lobatto (GLL) | Yes             | $2N - 3$        | $N - 2$                      |
    | Equiangular (CC)    | Yes             | $\approx N - 1$ | $\approx N/2$                |

    Parameters
    ----------
    nlat : int
        Number of latitude points
    grid : str, optional
        Grid type (``"legendre-gauss"``, ``"lobatto"``, ``"equiangular"``, ``"equiangular-trapezoidal"``), by default ``"equiangular"``

    Returns
    -------
    int
        Maximum spherical harmonic degree (non-inclusive)
    """
    if grid == "legendre-gauss":
        return nlat
    elif grid == "lobatto":
        return nlat - 1
    elif grid in ["equiangular", "equiangular-trapezoidal"]:
        warnings.warn(
            "Default SHT truncation changed in v0.9.0: equiangular/equiangular-trapezoidal grids now truncate to (nlat+1)//2. " "Specify lmax explicitly to override.",
            UserWarning,
            stacklevel=2,
        )
        return (nlat + 1) // 2
    else:
        raise ValueError(f"Unknown grid type {grid}")


def _truncate_mmax(nlon: int) -> int:
    """
    Truncate the maximum azimuthal harmonic degree based on the longitude grid. This is the same as the
    Nyquist frequency.

    Parameters
    ----------
    nlon : int
        Number of longitude points

    Returns
    -------
    int
        Maximum azimuthal harmonic degree (non-inclusive)
    """
    return nlon // 2 + 1


[docs] def truncate_sht(nlat: int, nlon: int, lmax: Optional[int] = None, mmax: Optional[int] = None, grid: Optional[str] = "equiangular") -> Tuple[int, int]: r""" Determine the maximum spherical harmonic degree and order for an SHT based on the spatial grid. When ``lmax`` or ``mmax`` are not provided, they are inferred from the grid resolution. The default truncation for each grid type is chosen so that the associated Legendre polynomials up to the returned degree can be square-integrated exactly by the corresponding quadrature rule: .. list-table:: Default latitudinal truncation :math:`l_{\max}` for :math:`N_\theta` latitude points :header-rows: 1 :widths: 30 15 25 30 * - Grid type - Includes poles? - Quadrature exactness - Default :math:`l_{\max}` * - ``"legendre-gauss"`` - No - :math:`2 N_\theta - 1` - :math:`N_\theta` * - ``"lobatto"`` - Yes - :math:`2 N_\theta - 3` - :math:`N_\theta - 1` * - ``"equiangular"`` / ``"equiangular-trapezoidal"`` - Yes - :math:`\approx N_\theta - 1` - :math:`\lfloor (N_\theta + 1) / 2 \rfloor` The default longitudinal truncation is the Nyquist limit of the uniform longitude grid: :math:`m_{\max} = \lfloor N_\lambda / 2 \rfloor + 1`. Finally, a **triangular truncation** is applied: :math:`l_{\max} = m_{\max} = \min(l_{\max},\, m_{\max})`, so that every retained degree has a full set of orders. Parameters ---------- nlat : int Number of latitude points :math:`N_\theta`. nlon : int Number of longitude points :math:`N_\lambda`. lmax : int, optional User-defined maximum spherical harmonic degree (non-inclusive). If not provided, the maximum degree is determined from the latitude grid as shown in the table above. mmax : int, optional User-defined maximum azimuthal harmonic order (non-inclusive). If not provided, set to the Nyquist limit :math:`\lfloor N_\lambda / 2 \rfloor + 1`. grid : str, optional Grid type (``"legendre-gauss"``, ``"lobatto"``, ``"equiangular"``, ``"equiangular-trapezoidal"``), by default ``"equiangular"``. Returns ------- lmax : int Maximum spherical harmonic degree (non-inclusive). mmax : int Maximum azimuthal harmonic order (non-inclusive). Examples -------- >>> from torch_harmonics import truncate_sht >>> truncate_sht(128, 256, grid="legendre-gauss") (128, 128) >>> truncate_sht(128, 256, grid="lobatto") (127, 127) >>> truncate_sht(128, 256, grid="equiangular") (64, 64) """ # determine the maximum degrees based on user-defined values or the default values based on the grid type lmax = lmax or _truncate_lmax(nlat, grid) mmax = mmax or _truncate_mmax(nlon) # perform triangular truncation lmax = min(lmax, mmax) mmax = lmax return lmax, mmax