Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0da47da
Create transform functions
paddyroddy Nov 18, 2025
69e5ec7
Finish inverse transform
paddyroddy Nov 19, 2025
4243882
Add transform function
paddyroddy Nov 19, 2025
52cbc7e
Add spin option to inverse transform
paddyroddy Nov 19, 2025
ee3590f
Add docs module
paddyroddy Nov 19, 2025
2a5924a
Fix issues
paddyroddy Nov 19, 2025
92329ab
Fix docs
paddyroddy Nov 19, 2025
f5ab1ee
Merge branch 'main' into paddy/issue-794
paddyroddy Nov 19, 2025
7fc40ff
Change to all groups
paddyroddy Nov 19, 2025
bf162e3
Add transform test
paddyroddy Nov 19, 2025
9e93982
Add inverse transform test
paddyroddy Nov 19, 2025
3e2d334
Fix uv doc
paddyroddy Nov 19, 2025
4e13567
Consistent parameters name
paddyroddy Nov 19, 2025
2cecde9
Remove unused GLASS imports
paddyroddy Nov 19, 2025
6dbe742
Fix `multalm` docs rendering
paddyroddy Nov 19, 2025
6707374
Remove leftover `inplace` arg
paddyroddy Nov 19, 2025
90a57b5
Add harmonics docs
paddyroddy Nov 19, 2025
79b0c57
Merge branch 'paddy/issue-807' into paddy/issue-794
paddyroddy Nov 19, 2025
bd120cb
Merge branch 'main' into paddy/issue-807
paddyroddy Nov 19, 2025
e69946a
Change to `from`
paddyroddy Nov 19, 2025
d119b3d
Merge branch 'paddy/issue-807' into paddy/issue-794
paddyroddy Nov 19, 2025
881fd30
Merge branch 'main' into paddy/issue-807
paddyroddy Nov 21, 2025
6e6f8b0
Merge branch 'paddy/issue-807' into paddy/issue-794
paddyroddy Nov 21, 2025
3bafd50
Skip tests due to healpy
paddyroddy Nov 21, 2025
855efcd
Merge branch 'main' into paddy/issue-807
paddyroddy Nov 25, 2025
55d597e
Merge branch 'paddy/issue-807' into paddy/issue-794
paddyroddy Nov 25, 2025
abc338e
Merge branch 'main' into paddy/issue-794
paddyroddy Dec 1, 2025
89471d1
Merge branch 'main' into paddy/issue-794
paddyroddy Dec 9, 2025
b412429
Merge branch 'main' into paddy/issue-794
paddyroddy Dec 10, 2025
714bec0
Merge branch 'main' into paddy/issue-794
paddyroddy Dec 12, 2025
b4c49ae
Merge branch 'main' into paddy/issue-794
paddyroddy Dec 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The developer installation of _GLASS_ comes with several optional dependencies -
These options can be used with `uv` in the following way

```bash
uv sync --all-extras --group docs --group test
uv sync --all-extras --all-groups
```

## Tooling
Expand Down
26 changes: 26 additions & 0 deletions docs/modules/harmonics.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.. module:: glass.harmonics

:mod:`glass.harmonics` --- Spherical harmonics utilities
========================================================

.. currentmodule:: glass.harmonics

This module contains utilities for working with spherical harmonics which are
used by GLASS, but are otherwise unrelated to GLASS functionality.

This module should be imported manually if used outside of GLASS::

import glass.harmonics


General
-------

.. autofunction:: multalm


Spherical harmonic transforms
-----------------------------

.. autofunction:: transform
.. autofunction:: inverse_transform
1 change: 1 addition & 0 deletions docs/modules/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Modules

algorithm
grf
harmonics
4 changes: 2 additions & 2 deletions glass/algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ def cov_clip(
The relative tolerance *rtol* is defined as for
:func:`~array_api.linalg.matrix_rank`.

Parameter
---------
Parameters
----------
cov
A symmetric matrix (or a stack of matrices).
rtol
Expand Down
5 changes: 3 additions & 2 deletions glass/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import array_api_compat
import array_api_extra as xpx

import glass
import glass.grf
import glass.harmonics

Expand Down Expand Up @@ -407,7 +406,9 @@ def _generate_grf(

# transform alm to maps
# can be performed in place on the temporary alm array
yield hp.alm2map(alm, nside, pixwin=False, pol=False, inplace=True)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pixwin defaults to False

yield glass.harmonics.inverse_transform(
alm, nside=nside, polarised_input=False, inplace=True
)


@deprecated("use glass.generate() instead")
Expand Down
1 change: 0 additions & 1 deletion glass/galaxies.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

import array_api_compat

import glass
import glass._array_api_utils as _utils
import glass.arraytools

Expand Down
112 changes: 102 additions & 10 deletions glass/harmonics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

from __future__ import annotations

import collections.abc
from typing import TYPE_CHECKING

import healpy as hp

import array_api_compat

if TYPE_CHECKING:
Expand All @@ -17,24 +20,22 @@ def multalm(
"""
Multiply alm by bl.

The alm should be in GLASS order:
The alm should be in GLASS order::

[
00,
10, 11,
20, 21, 22,
30, 31, 32, 33,
...
]
[
00,
10, 11,
20, 21, 22,
30, 31, 32, 33,
...
]

Parameters
----------
alm
The alm to multiply.
bl
The bl to multiply.
inplace
Whether to perform the operation in place.

Returns
-------
Expand All @@ -43,3 +44,94 @@ def multalm(
"""
xp = array_api_compat.array_namespace(alm, bl, use_compat=False)
return alm * xp.repeat(bl, xp.arange(bl.size) + 1)


def transform(
maps: FloatArray,
*,
lmax: int | None = None,
polarised_input: bool = True,
use_pixel_weights: bool = False,
) -> ComplexArray:
"""
Computes the spherical harmonic transform.

Parameters
----------
maps
The input map(s).
lmax
The maximum multipole to use.
polarised_input
Whether the input maps represent polarised data.
use_pixel_weights
Whether to use pixel weights in the transform.

Returns
-------
The spherical harmonic coefficients.

"""
return hp.map2alm(
maps,
lmax=lmax,
pol=polarised_input,
use_pixel_weights=use_pixel_weights,
)


def inverse_transform( # noqa: PLR0913
alm: ComplexArray | collections.abc.Sequence[ComplexArray],
*,
nside: int,
inplace: bool = False,
lmax: int | None = None,
polarised_input: bool = True,
spin: int | None = None,
) -> FloatArray | list[FloatArray]:
"""
Computes the inverse spherical harmonic transform.

Parameters
----------
alm
The spherical harmonic coefficients. If computing a spin-s map (spin is
not None), this should be a sequence of two coefficient arrays:
[alm, blm]. Otherwise, it is the single alm array.
nside
The nside of the output map if using HEALPix.
inplace
Whether to perform the operation in place (only applicable when
spin=None).
lmax
The maximum multipole to use.
polarised_input
Whether the input alm represents polarised data (only applicable when
spin=None).
spin
The spin s of the field being transformed.

Returns
-------
The output map(s).

"""
if spin is None:
# alm is the single alm array or a sequence/array of 3 for polarised data
return hp.alm2map(
alm,
nside,
inplace=inplace,
lmax=lmax,
pol=polarised_input,
)

# check if alm is of the correct form for spin transforms
if not (hasattr(alm, "shape") and alm.shape[0] == 2) and not (
isinstance(alm, collections.abc.Sequence) and len(alm) == 2
):
# alm must be a sequence [alm, blm]
msg = "for spin transforms, alm must be of the form [alm, blm]"
raise ValueError(msg)
# does not accept "inplace" or "polarised_input" arguments
return hp.alm2map_spin(alm, nside, spin, lmax)
22 changes: 15 additions & 7 deletions glass/lensing.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@
import array_api_compat

import glass._array_api_utils as _utils
import glass.harmonics

if TYPE_CHECKING:
from collections.abc import Sequence
from types import ModuleType

import glass
from glass._types import ComplexArray, FloatArray
from glass.cosmology import Cosmology

Expand Down Expand Up @@ -279,7 +279,9 @@ def from_convergence( # noqa: PLR0913
lmax = 3 * nside - 1

# compute alm
alm = hp.map2alm(kappa, lmax=lmax, pol=False, use_pixel_weights=True)
alm = glass.harmonics.transform(
kappa, lmax=lmax, polarised_input=False, use_pixel_weights=True
)

# mode number; all conversions are factors of this
ell = np.arange(lmax + 1)
Expand All @@ -293,7 +295,7 @@ def from_convergence( # noqa: PLR0913

# if potential is requested, compute map and add to output
if potential:
psi = hp.alm2map(alm, nside, lmax=lmax)
psi = glass.harmonics.inverse_transform(alm, nside=nside, lmax=lmax)
results += (psi,)

# if no spin-weighted maps are requested, stop here
Expand All @@ -310,7 +312,9 @@ def from_convergence( # noqa: PLR0913

# if deflection is requested, compute spin-1 maps and add to output
if deflection:
alpha = hp.alm2map_spin([alm, blm], nside, 1, lmax)
alpha = glass.harmonics.inverse_transform(
[alm, blm], nside=nside, spin=1, lmax=lmax
)
alpha = alpha[0] + 1j * alpha[1]
results += (alpha,)

Expand All @@ -328,7 +332,9 @@ def from_convergence( # noqa: PLR0913
hp.almxfl(alm, fl, inplace=True)

# transform to shear maps
gamma = hp.alm2map_spin([alm, blm], nside, 2, lmax)
gamma = glass.harmonics.inverse_transform(
[alm, blm], nside=nside, spin=2, lmax=lmax
)
gamma = gamma[0] + 1j * gamma[1]
results += (gamma,)

Expand Down Expand Up @@ -370,7 +376,9 @@ def shear_from_convergence(
lmax = 3 * nside - 1

# compute alm
alm = hp.map2alm(kappa, lmax=lmax, pol=False, use_pixel_weights=True)
alm = glass.harmonics.transform(
kappa, lmax=lmax, polarised_input=False, use_pixel_weights=True
)

# zero B-modes
blm = np.zeros_like(alm)
Expand All @@ -390,7 +398,7 @@ def shear_from_convergence(
hp.almxfl(alm, fl, inplace=True)

# transform to shear maps
return hp.alm2map_spin([alm, blm], nside, 2, lmax)
return glass.harmonics.inverse_transform([alm, blm], nside=nside, spin=2, lmax=lmax)


class MultiPlaneConvergence:
Expand Down
1 change: 0 additions & 1 deletion glass/points.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@

import array_api_compat

import glass
import glass._array_api_utils as _utils
import glass.arraytools

Expand Down
75 changes: 75 additions & 0 deletions tests/core/test_harmonics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import TYPE_CHECKING

import healpy as hp
import numpy as np
import pytest

Expand Down Expand Up @@ -57,3 +58,77 @@ def test_multalm(xp: ModuleType) -> None:

result = glass.harmonics.multalm(alm, bl)
np.testing.assert_allclose(result, alm)


def test_transform(xp: ModuleType) -> None:
# prepare inputs
constant = 1.23
lmax = 0
nside = 8

# create an unpolarised map where every pixel is the same
simple_map = xp.full(hp.nside2npix(nside), constant)

a00 = xp.sqrt(4 * xp.pi) * constant
# the alm array for lmax=0 has size 1, so it only contains a_00
alm_expected = xp.asarray([a00], dtype=xp.complex128)

alm_result = glass.harmonics.transform(
simple_map,
lmax=lmax,
polarised_input=False,
)

np.testing.assert_allclose(alm_result, alm_expected, rtol=1e-15)


def test_inverse_transform(xp: ModuleType) -> None:
# prepare inputs
lmax = 1
nside = 8
spin = 1

# create a simple monopole component ensuring a predictable output map value
alm_size = hp.Alm.getsize(lmax)
alm_monopole = xp.zeros(alm_size, dtype=xp.complex128)
alm_monopole[0] = xp.sqrt(4 * xp.pi)

# standard unpolarised transform without spin

map_result = glass.harmonics.inverse_transform(
alm_monopole,
lmax=lmax,
nside=nside,
polarised_input=False,
)

assert hasattr(map_result, "ndim")
assert map_result.ndim == 1
np.testing.assert_array_equal(map_result, 1.0)

# valid spin transform

alm_input_list = [alm_monopole, alm_monopole]

map_result_spin = glass.harmonics.inverse_transform(
alm_input_list,
lmax=lmax,
nside=nside,
spin=spin,
)

assert isinstance(map_result_spin, list)
assert len(map_result_spin) == 2
assert all(hasattr(m, "ndim") and m.ndim == 1 for m in map_result_spin)

# invalid spin input

with pytest.raises(
ValueError, match="for spin transforms, alm must be of the form"
):
glass.harmonics.inverse_transform(
alm_monopole,
lmax=lmax,
nside=nside,
spin=spin,
)