"""Convolution functions specifically tailored to Lezargus containers.
We seperate the logic for containers into functions which make it a little
easier to understand. Moreover, these functions can also be used separately.
The logic is similar to Numpy's functions like py:func:`numpy.mean` and
`numpy.ndarray.mean`; and other modules in
:py:mod:`lezargus.container.function`.
"""
# isort: split
# Import required to remove circular dependencies from type checking.
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from lezargus.library import hint
# isort: split
import numpy as np
import lezargus
from lezargus.library import logging
[docs]
def convolve_spectrum_by_spectral_kernel(
spectrum: hint.LezargusSpectrum,
kernel: hint.ndarray | None = None,
kernel_stack: hint.ndarray | None = None,
kernel_function: hint.Callable | None = None,
) -> hint.LezargusSpectrum:
"""Convolve the spectrum with a spectral kernel.
We compute the convolution and return a near copy of the spectrum after
convolution. The wavelength is not affected.
As spectrum are 1D, there is no dimension to have a variable kernel,
as variable kernels need a non-convolution axis to vary on. There can
only be a static or dynamic kernel.
Parameters
----------
spectrum : LezargusCube
The spectrum we are convolving.
kernel : ndarray, default = None
A static 1D spectral kernel. If provided, we use this static kernel
to convolve the spectrum by. Exclusive with other kernel options.
kernel_stack : ndarray, default = None
A variable 1D spectral kernel stack. If provided, we use the
variable kernel stack to convolve the spectrum by. Exclusive with
other kernel options.
kernel_function : Callable, default = None
A dynamic 1D kernel function. If provided, we use the dynamic kernel
function to convolve the spectrum by. Exclusive with other kernel
options.
Returns
-------
convolved_spectrum : ndarray
A near copy of the spectrum after convolution.
"""
# Determine the kernel used, and the convolution function being used.
is_static = kernel is not None
is_variable = kernel_stack is not None
is_dynamic = kernel_function is not None
if sum((is_static, is_variable, is_dynamic)) != 1:
logging.error(
error_type=logging.InputError,
message=(
f"Only one kernel type allowed: static, {is_static};"
f" variable, {is_variable}; dynamic, {is_dynamic}."
),
)
# We assume that variances add; thus uncertainties add in quadrature.
variance = spectrum.uncertainty**2
# We convolve based on the method of convolution as specified.
if is_static:
# Static convolution.
convolved_data = lezargus.library.convolution.static_1d_with_1d(
array=spectrum.data,
kernel=kernel,
)
convolved_variance = lezargus.library.convolution.static_1d_with_1d(
array=variance,
kernel=kernel,
)
elif is_variable:
logging.warning(
warning_type=logging.AlgorithmWarning,
message=(
"Variable kernel convolution with a Spectrum undefined, no"
" non-convolution axis. Assuming static kernel."
),
)
# We still try static convolution. Will likely fail.
convolved_data = lezargus.library.convolution.static_1d_with_1d(
array=spectrum.data,
kernel=kernel,
)
convolved_variance = lezargus.library.convolution.static_1d_with_1d(
array=variance,
kernel=kernel,
)
elif is_dynamic:
# Dynamic convolution.
convolved_data = None
convolved_variance = None
logging.critical(
critical_type=logging.ToDoError,
message="No cube dynamic convolution.",
)
else:
# One of the above convolutions should have triggered.
convolved_data = spectrum.data
convolved_variance = variance
logging.error(
error_type=logging.LogicFlowError,
message=(
f"Unknown convolution mode: static, {is_static}; variable,"
f" {is_variable}; dynamic, {is_dynamic}."
),
)
# We also propagate the convolution of the mask and the flags where
# needed.
logging.error(
error_type=logging.ToDoError,
message="Propagation of mask and flags via convolution is not done.",
)
convolved_mask = spectrum.mask
convolved_flags = spectrum.flags
# Converting back to uncertainty.
convolved_uncertainty = np.sqrt(convolved_variance)
# From the above information, we construct the new spectrum.
spectrum_class = type(spectrum)
convolved_spectrum = spectrum_class(
wavelength=spectrum.wavelength,
data=convolved_data,
uncertainty=convolved_uncertainty,
wavelength_unit=spectrum.wavelength_unit,
data_unit=spectrum.data_unit,
mask=convolved_mask,
flags=convolved_flags,
header=spectrum.header,
)
# All done.
return convolved_spectrum
[docs]
def convolve_cube_by_spectral_kernel(
cube: hint.LezargusCube,
kernel: hint.ndarray | None = None,
kernel_stack: hint.ndarray | None = None,
kernel_function: hint.Callable | None = None,
) -> hint.LezargusCube:
"""Convolve the cube by a spectral kernel convolving spectra slices.
Convolving a spectral cube can either be done one of two ways;
convolving by image slices or convolving by spectral slices. We here
convolve by spectral slices.
Parameters
----------
cube : LezargusCube
The cube we are convolving.
kernel : ndarray, default = None
A static 1D spectral kernel. If provided, we use this static kernel
to convolve the cube by. Exclusive with other kernel options.
kernel_stack : ndarray, default = None
A variable 1D spectral kernel stack. If provided, we use the
variable kernel stack to convolve the cube by. Exclusive with
other kernel options.
kernel_function : Callable, default = None
A dynamic 1D kernel function. If provided, we use the dynamic kernel
function to convolve the cube by. Exclusive with other kernel
options.
Returns
-------
convolved_cube : ndarray
A near copy of the data cube after convolution.
"""
# Determine the kernel used, and the convolution function being used.
is_static = kernel is not None
is_variable = kernel_stack is not None
is_dynamic = kernel_function is not None
if sum((is_static, is_variable, is_dynamic)) != 1:
logging.error(
error_type=logging.InputError,
message=(
f"Only one kernel type allowed: static, {is_static};"
f" variable, {is_variable}; dynamic, {is_dynamic}."
),
)
# We assume that variances add; thus uncertainties add in quadrature.
variance = cube.uncertainty**2
# We convolve based on the method of convolution as specified.
if is_static:
# Static convolution.
convolved_data = lezargus.library.convolution.static_3d_with_1d_over_z(
array=cube.data,
kernel=kernel,
)
convolved_variance = (
lezargus.library.convolution.static_3d_with_1d_over_z(
array=variance,
kernel=kernel,
)
)
elif is_variable:
# Variable convolution.
convolved_data = None
convolved_variance = None
logging.critical(
critical_type=logging.ToDoError,
message="No cube by spectra variable convolution.",
)
elif is_dynamic:
# Dynamic convolution.
convolved_data = None
convolved_variance = None
logging.critical(
critical_type=logging.ToDoError,
message="No cube by spectra dynamic convolution.",
)
else:
# One of the above convolutions should have triggered.
convolved_data = cube.data
convolved_variance = variance
logging.error(
error_type=logging.LogicFlowError,
message=(
f"Unknown convolution mode: static, {is_static}; variable,"
f" {is_variable}; dynamic, {is_dynamic}."
),
)
# We also propagate the convolution of the mask and the flags where
# needed.
logging.error(
error_type=logging.ToDoError,
message="Propagation of mask and flags via convolution is not done.",
)
convolved_mask = cube.mask
convolved_flags = cube.flags
# Converting back to uncertainty.
convolved_uncertainty = np.sqrt(convolved_variance)
# From the above information, we construct the new spectra.
cube_class = type(cube)
convolved_cube = cube_class(
wavelength=cube.wavelength,
data=convolved_data,
uncertainty=convolved_uncertainty,
wavelength_unit=cube.wavelength_unit,
data_unit=cube.data_unit,
spectral_scale=cube.spectral_scale,
pixel_scale=cube.pixel_scale,
slice_scale=cube.slice_scale,
mask=convolved_mask,
flags=convolved_flags,
header=cube.header,
)
# All done.
return convolved_cube
[docs]
def convolve_cube_by_image_kernel(
cube: hint.LezargusCube,
kernel: hint.ndarray | None = None,
kernel_stack: hint.ndarray | None = None,
kernel_function: hint.Callable | None = None,
) -> hint.LezargusCube:
"""Convolve the cube by an image kernel convolving image slices.
Convolving a spectral cube can either be done one of two ways;
convolving by image slices or convolving by spectral slices. We here
convolve by image slices.
Parameters
----------
cube : LezargusCube
The cube we are convolving.
kernel : ndarray, default = None
A static 2D image kernel. If provided, we use this static kernel
to convolve the cube by. Exclusive with other kernel options.
kernel_stack : ndarray, default = None
A variable 2D image kernel stack. If provided, we use the variable
kernel stack to convolve the cube by. Exclusive with other kernel
options.
kernel_function : Callable, default = None
A dynamic 2D kernel function. If provided, we use the dynamic
kernel function to convolve the cube by. Exclusive with other
kernel options.
Returns
-------
convolved_cube : ndarray
A near copy of the data cube after convolution.
"""
# Determine the kernel used, and the convolution function being used.
is_static = kernel is not None
is_variable = kernel_stack is not None
is_dynamic = kernel_function is not None
if sum((is_static, is_variable, is_dynamic)) != 1:
logging.error(
error_type=logging.InputError,
message=(
f"Only one kernel type allowed: static, {is_static};"
f" variable, {is_variable}; dynamic, {is_dynamic}."
),
)
# We assume that variances add; thus uncertainties add in quadrature.
variance = cube.uncertainty**2
# We convolve based on the method of convolution as specified.
if is_static:
# Static convolution.
convolved_data = lezargus.library.convolution.static_3d_with_2d_over_xy(
array=cube.data,
kernel=kernel,
)
convolved_variance = (
lezargus.library.convolution.static_3d_with_2d_over_xy(
array=variance,
kernel=kernel,
)
)
elif is_variable:
# Variable convolution.
convolved_data = (
lezargus.library.convolution.variable_3d_with_2d_over_xy(
array=cube.data,
kernel_stack=kernel_stack,
)
)
convolved_variance = (
lezargus.library.convolution.variable_3d_with_2d_over_xy(
array=variance,
kernel_stack=kernel_stack,
)
)
elif is_dynamic:
# Dynamic convolution.
convolved_data = None
convolved_variance = None
logging.critical(
critical_type=logging.ToDoError,
message="No cube by image dynamic convolution.",
)
else:
# One of the above convolutions should have triggered.
convolved_data = cube.data
convolved_variance = variance
logging.error(
error_type=logging.LogicFlowError,
message=(
f"Unknown convolution mode: static, {is_static}; variable,"
f" {is_variable}; dynamic, {is_dynamic}."
),
)
# We also propagate the convolution of the mask and the flags where
# needed.
logging.error(
error_type=logging.ToDoError,
message="Propagation of mask and flags via convolution is not done.",
)
convolved_mask = cube.mask
convolved_flags = cube.flags
# Converting back to uncertainty.
convolved_uncertainty = np.sqrt(convolved_variance)
# From the above information, we construct the new spectra.
cube_class = type(cube)
convolved_cube = cube_class(
wavelength=cube.wavelength,
data=convolved_data,
uncertainty=convolved_uncertainty,
wavelength_unit=cube.wavelength_unit,
data_unit=cube.data_unit,
spectral_scale=cube.spectral_scale,
pixel_scale=cube.pixel_scale,
slice_scale=cube.slice_scale,
mask=convolved_mask,
flags=convolved_flags,
header=cube.header,
)
# All done.
return convolved_cube