Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: discrete mapper with ducc #155

Merged
merged 1 commit into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .commitlint.rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {
"catalog",
"cli",
"core",
"ducc",
"fields",
"healpy",
"io",
Expand Down
29 changes: 25 additions & 4 deletions heracles/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,18 +165,25 @@ def mapper_from_config(config, section):
choices = {
"none": "none",
"healpix": "healpix",
"discrete": "discrete",
}

mapper = config.getchoice(section, "mapper", choices)
if mapper == "none":
return None

if mapper == "healpix":
from .healpy import HealpixMapper

nside = config.getint(section, "nside")
lmax = config.getint(section, "lmax", fallback=None)
deconvolve = config.getboolean(section, "deconvolve", fallback=None)
return HealpixMapper(nside, lmax, deconvolve=deconvolve)

if mapper == "discrete":
from .ducc import DiscreteMapper

lmax = config.getint(section, "lmax", fallback=None)
return DiscreteMapper(lmax)

return None


Expand Down Expand Up @@ -223,6 +230,12 @@ def catalog_from_config(config, section, label=None, *, out=None):
# check if visibility is per catalogue or per selection
visibility: str | Mapping[str, str]
visibility = config.get(section, "visibility", fallback=None)
visibility_transform = config.getboolean(
section,
"visibility-transform",
fallback=False,
)
visibility_lmax = config.getint(section, "visibility-lmax", fallback=None)
# check if visibility is a mapping
if visibility and "\n" in visibility:
visibility = config.getdict(section, "visibility")
Expand All @@ -233,7 +246,11 @@ def catalog_from_config(config, section, label=None, *, out=None):
# set base catalogue's visibility if just one was given
if isinstance(visibility, str):
try:
vmap = read_vmap(getpath(visibility))
vmap = read_vmap(
getpath(visibility),
transform=visibility_transform,
lmax=visibility_lmax,
)
except (TypeError, ValueError, OSError) as exc:
msg = f"Cannot load visibility: {exc!s}"
raise ValueError(msg)
Expand Down Expand Up @@ -269,7 +286,11 @@ def catalog_from_config(config, section, label=None, *, out=None):
msg = f"Invalid value: unknown selection '{num}'"
raise ValueError(msg)
try:
vmap = read_vmap(getpath(value))
vmap = read_vmap(
getpath(value),
transform=visibility_transform,
lmax=visibility_lmax,
)
except (TypeError, ValueError, OSError) as exc:
msg = f"Cannot load visibility: {exc!s}"
raise ValueError(msg)
Expand Down
163 changes: 163 additions & 0 deletions heracles/ducc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Heracles: Euclid code for harmonic-space statistics on the sphere
#
# Copyright (C) 2023-2024 Euclid Science Ground Segment
#
# This file is part of Heracles.
#
# Heracles is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Heracles is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with Heracles. If not, see <https://www.gnu.org/licenses/>.
"""
Module for discrete spherical harmonic transforms with ducc.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import ducc0
import numpy as np

from heracles.core import update_metadata

if TYPE_CHECKING:
from collections.abc import Mapping
from typing import Any

from numpy.typing import ArrayLike, DTypeLike, NDArray


class DiscreteMapper:
"""
Mapper that creates alms directly.
"""

def __init__(
self,
lmax: int,
*,
dtype: DTypeLike = np.complex128,
nthreads: int = 0,
) -> None:
"""
Mapper for alms.
"""
self.__lmax = lmax
self.__dtype = np.dtype(dtype)
self.__nthreads = nthreads

@property
def lmax(self) -> int:
"""
The maximum angular mode number.
"""
return self.__lmax

@property
def area(self) -> float:
"""
The effective area for this mapper.
"""
return 1.0

def create(
self,
*dims: int,
spin: int = 0,
) -> NDArray[Any]:
"""
Create zero alms.
"""
lmax = self.__lmax
m = np.zeros((*dims, (lmax + 1) * (lmax + 2) // 2), dtype=self.__dtype)
update_metadata(
m,
geometry="discrete",
kernel="none",
lmax=lmax,
spin=spin,
)
return m

def map_values(
self,
lon: NDArray[Any],
lat: NDArray[Any],
data: NDArray[Any],
values: NDArray[Any],
) -> None:
"""
Add values to alms.
"""

md: Mapping[str, Any] = data.dtype.metadata or {}

flatten = values.ndim == 1
if flatten:
values = values.reshape(1, -1)

epsilon: float
if values.dtype == np.float64:
epsilon = 1e-12
elif values.dtype == np.float32:
epsilon = 1e-5
else:
values = values.astype(np.float64)
epsilon = 1e-12

spin = md.get("spin", 0)

loc = np.empty((lon.size, 2), dtype=np.float64)
loc[:, 0] = np.radians(90.0 - lat)
loc[:, 1] = np.radians(lon % 360.0)

alms = ducc0.sht.adjoint_synthesis_general(
map=values,
spin=spin,
lmax=self.__lmax,
loc=loc,
epsilon=epsilon,
nthreads=self.__nthreads,
)

if flatten:
alms = alms[0]

data += alms

def transform(
self,
data: ArrayLike,
) -> ArrayLike | tuple[ArrayLike, ArrayLike]:
"""
Does nothing, since inputs are alms already.
"""
return data

def resample(self, data: NDArray[Any]) -> NDArray[Any]:
"""
Change LMAX of alm.
"""
*dims, n = data.shape
lmax_in = (int((8 * n + 1) ** 0.5 + 0.01) - 3) // 2
lmax_out = self.__lmax
lmax = min(lmax_in, lmax_out)
out = np.zeros(
(*dims, (lmax_out + 1) * (lmax_out + 2) // 2),
dtype=self.__dtype,
)
i = j = 0
for m in range(lmax + 1):
out[..., j : j + lmax - m + 1] = data[..., i : i + lmax - m + 1]
i += lmax_in - m + 1
j += lmax_out - m + 1
return out
5 changes: 4 additions & 1 deletion heracles/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ def _read_twopoint(hdu):
return arr


def read_vmap(filename, nside=None, field=0):
def read_vmap(filename, nside=None, field=0, *, transform=False, lmax=None):
"""read visibility map from a HEALPix map file"""

import healpy as hp
Expand All @@ -315,6 +315,9 @@ def read_vmap(filename, nside=None, field=0):
warn(f"{filename}: changing NSIDE to {nside}")
vmap = hp.ud_grade(vmap, nside)

if transform:
vmap = hp.map2alm(vmap, lmax=lmax, use_pixel_weights=True)

return vmap


Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ optional-dependencies = {all = [
"sphinx",
"sphinxcontrib-katex",
], test = [
"ducc0",
"pytest",
"pytest-rerunfailures",
]}
Expand Down
8 changes: 5 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ def test_catalog_from_config(mock):
0 = vmap.0.fits
2 = vmap.2.fits
""",
"visibility-transform": "true",
"visibility-lmax": "100",
},
},
)
Expand All @@ -213,7 +215,7 @@ def test_catalog_from_config(mock):
assert catalog[0].visibility is catalog[0].base.visibility
assert catalog[1].visibility is catalog[0].base.visibility
assert catalog[2].visibility is catalog[0].base.visibility
assert mock.call_args_list == [(("vmap.fits",),)]
assert mock.call_args_list == [(("vmap.fits",), dict(transform=False, lmax=None))]

mock.reset_mock()

Expand All @@ -236,8 +238,8 @@ def test_catalog_from_config(mock):
assert catalog[1].visibility is None
assert catalog[2].visibility is mock.return_value
assert mock.call_args_list == [
(("vmap.0.fits",),),
(("vmap.2.fits",),),
(("vmap.0.fits",), dict(transform=True, lmax=100)),
(("vmap.2.fits",), dict(transform=True, lmax=100)),
]

with pytest.raises(ValueError, match="Duplicate selection"):
Expand Down
45 changes: 45 additions & 0 deletions tests/test_ducc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import importlib.util

import numpy as np
import numpy.testing as npt
import pytest

HAVE_DUCC = importlib.util.find_spec("ducc0") is not None

skipif_no_ducc = pytest.mark.skipif(not HAVE_DUCC, reason="test requires ducc")


@skipif_no_ducc
def test_resample():
from heracles.ducc import DiscreteMapper

lmax = 200

alm = np.concatenate(
[np.arange(m, lmax + 1) for m in range(lmax + 1)],
dtype=complex,
)

out = DiscreteMapper(lmax).resample(alm)

npt.assert_array_equal(out, alm)

lmax_out = lmax // 2
out = DiscreteMapper(lmax_out).resample(alm)

assert out.shape == ((lmax_out + 1) * (lmax_out + 2) // 2,)
i = j = 0
for m in range(lmax_out + 1):
i, j = j, j + lmax_out - m + 1
npt.assert_array_equal(out[i:j], np.arange(m, lmax_out + 1))

lmax_out = lmax * 2
out = DiscreteMapper(lmax_out).resample(alm)

assert out.shape == ((lmax_out + 1) * (lmax_out + 2) // 2,)
i = j = 0
for m in range(lmax + 1):
i, j = j, j + lmax_out - m + 1
expected = np.pad(np.arange(m, lmax + 1), (0, lmax_out - lmax))
npt.assert_array_equal(out[i:j], expected)
npt.assert_array_equal(out[j:], 0.0)
Loading