# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""Utilities related to wavelength bin calculations."""
# THIRD-PARTY
import numpy as np
# ASTROPY
from astropy import units as u
# LOCAL
from synphot import exceptions
__all__ = ['calculate_bin_edges', 'calculate_bin_widths',
'calculate_bin_centers', 'wave_range', 'pixel_range']
def _slow_calcbinflux(len_binwave, i_beg, i_end, avflux, deltaw):
"""Python implementation of ``calcbinflux``.
This is only used if ``synphot.synphot_utils`` C-extension
import fails.
See ``include/synphot_utils.h``.
"""
binflux = np.empty(shape=(len_binwave, ), dtype=np.float64)
intwave = np.empty(shape=(len_binwave, ), dtype=np.float64)
# Note that, like all Python striding, the range over which
# we integrate is [first:last).
for i in range(len(i_beg)):
first = i_beg[i]
last = i_end[i]
cur_dw = deltaw[first:last]
intwave[i] = cur_dw.sum()
binflux[i] = np.sum(avflux[first:last] * cur_dw) / intwave[i]
return binflux, intwave
# Try to import the C version of calcbinflux, otherwise fall back
# to the Python implementation above.
try:
from synphot import synphot_utils
except ImportError:
calcbinflux = _slow_calcbinflux
else:
calcbinflux = synphot_utils.calcbinflux
[docs]
def calculate_bin_edges(centers):
"""Calculate the edges of wavelength bins given the centers.
The algorithm calculates bin edges as the midpoints between bin centers
and treats the first and last bins as symmetric about their centers.
Parameters
----------
centers : array-like or `~astropy.units.quantity.Quantity`
Sequence of bin centers. Must be 1D and have at least two values.
If not a Quantity, assumed to be in Angstrom.
Returns
-------
edges : `~astropy.units.quantity.Quantity`
Array of bin edges. Will be 1D, have one more value
than ``centers``, and also the same unit.
Raises
------
synphot.exceptions.SynphotError
Invalid input.
"""
if not isinstance(centers, u.Quantity):
centers = centers * u.AA
if centers.ndim != 1:
raise exceptions.SynphotError('Bin centers must be 1D array.')
if centers.size < 2:
raise exceptions.SynphotError(
'Bin centers must have at least two values.')
edges = np.empty(centers.size + 1, dtype=np.float64)
edges[1:-1] = (centers.value[1:] + centers.value[:-1]) * 0.5
# Compute the first and last by making them symmetric
edges[0] = 2.0 * centers.value[0] - edges[1]
edges[-1] = 2.0 * centers.value[-1] - edges[-2]
return edges * centers.unit
[docs]
def calculate_bin_widths(edges):
"""Calculate the widths of wavelengths bins given their edges.
Parameters
----------
edges : array-like or `~astropy.units.quantity.Quantity`
Sequence of bin edges. Must be 1D and have at least two values.
If not a Quantity, assumed to be in Angstrom.
Returns
-------
widths : `~astropy.units.quantity.Quantity`
Array of bin widths. Will be 1D, have one less value
than ``edges``, and also the same unit.
Raises
------
synphot.exceptions.SynphotError
Invalid input.
"""
if not isinstance(edges, u.Quantity):
edges = edges * u.AA
if edges.ndim != 1:
raise exceptions.SynphotError('Bin edges must be 1D array.')
if edges.size < 2:
raise exceptions.SynphotError(
'Bin edges must have at least two values.')
return np.abs(edges[1:] - edges[:-1])
[docs]
def calculate_bin_centers(edges):
"""Calculate the centers of wavelengths bins given their edges.
Parameters
----------
edges : array-like or `~astropy.units.quantity.Quantity`
Sequence of bin edges. Must be 1D and have at least two values.
If not a Quantity, assumed to be in Angstrom.
Returns
-------
centers : `~astropy.units.quantity.Quantity`
Array of bin centers. Will be 1D, have one less value
than ``edges``, and also the same unit.
Raises
------
synphot.exceptions.SynphotError
Invalid input.
"""
if not isinstance(edges, u.Quantity):
edges = edges * u.AA
if edges.ndim != 1:
raise exceptions.SynphotError('Bin edges must be 1D array.')
if edges.size < 2:
raise exceptions.SynphotError(
'Bin edges must have at least two values.')
centers = np.empty(edges.size - 1, dtype=np.float64)
centers[0] = edges.value[:2].mean()
for i in range(1, centers.size):
centers[i] = 2.0 * edges.value[i] - centers[i - 1]
return centers * edges.unit
[docs]
def wave_range(bins, cenwave, npix, mode='round'):
"""Calculate the wavelength range covered by the given number of pixels
centered on the given central wavelength of the given bins.
Parameters
----------
bins : array-like
Wavelengths at bin centers, each centered on a pixel.
Must be 1D array.
cenwave : float
Desired central wavelength, in the same unit as ``bins``.
npix : int
Desired number of pixels, centered on ``cenwave``.
mode : {'round', 'min', 'max', 'none'}
Determines how the pixels at the edges of the wavelength range
are handled. All the options, except 'none', will return
wavelength range edges that correspond to pixel edges:
* 'round' - Wavelength range edges are the pixel edges
and the range spans exactly ``npix`` pixels. An edge
that falls in the center of a bin is rounded to the
nearest pixel edge. This is the default.
* 'min' - Wavelength range is shrunk such that it includes
an integer number of pixels and its edges fall on pixel
edges. It may not span exactly ``npix`` pixels.
* 'max' - Wavelength range is expanded such that it
includes an integer number of pixels and its edges fall
on pixel edges. It may not span exactly ``npix`` pixels.
* 'none' - Exact wavelength range is returned. The edges
may not correspond to pixel edges, but it covers exactly
``npix`` pixels.
Returns
-------
wave1, wave2 : float
Lower and upper limits of the wavelength range.
Raises
------
synphot.exceptions.OverlapError
Given central wavelength is not within the given bins
or the wavelength range would exceed the bin limits.
synphot.exceptions.SynphotError
Invalid inputs or calculation failed.
"""
mode = mode.lower()
if mode not in ('round', 'min', 'max', 'none'):
raise exceptions.SynphotError(
'mode={0} is invalid, must be "round", "min", "max", '
'or "none".'.format(mode))
if not isinstance(npix, int):
raise exceptions.SynphotError('npix={0} is invalid.'.format(npix))
# Bin values must be in ascending order.
if bins[0] > bins[-1]:
bins = bins[::-1]
# Central wavelength must be within given bins.
if cenwave < bins[0] or cenwave > bins[-1]:
raise exceptions.OverlapError(
'cenwave={0} is not within binset (min={1}, max={2}).'.format(
cenwave, bins[0], bins[-1]))
# Find the index the central wavelength among bins
diff = cenwave - bins
ind = np.argmin(np.abs(diff))
# Calculate fractional index
frac_ind = float(ind)
if diff[ind] < 0:
frac_ind += diff[ind] / (bins[ind] - bins[ind - 1])
elif diff[ind] > 0:
frac_ind += diff[ind] / (bins[ind + 1] - bins[ind])
# Calculate fractional indices of the edges
half_npix = npix / 2.0
frac_ind1 = frac_ind - half_npix
frac_ind2 = frac_ind + half_npix
# Calculated edges must not exceed bin edges
if frac_ind1 < -0.5:
raise exceptions.OverlapError(
'Lower limit of wavelength range is out of bounds.')
if frac_ind2 > (bins.size - 0.5):
raise exceptions.OverlapError(
'Upper limit of wavelength range is out of bounds.')
frac1, int1 = np.modf(frac_ind1)
frac2, int2 = np.modf(frac_ind2)
int1 = int(int1)
int2 = int(int2)
if mode == 'round':
# Lower end of wavelength range
if frac1 >= 0:
# end is somewhere greater than binset[0] so we can just
# interpolate between two neighboring values going with upper edge
wave1 = bins[int1:int1 + 2].mean()
else:
# end is below the lowest binset value, but not by enough to
# trigger an exception
wave1 = bins[0] - (bins[0:2].mean() - bins[0])
# Upper end of wavelength range
if int2 < bins.shape[0] - 1:
# end is somewhere below binset[-1] so we can just interpolate
# between two neighboring values, going with the upper edge.
wave2 = bins[int2:int2 + 2].mean()
else:
# end is above highest binset value but not by enough to
# trigger an exception
wave2 = bins[-1] + (bins[-1] - bins[-2:].mean())
elif mode == 'min':
# Lower end of wavelength range
if frac1 <= 0.5 and int1 < bins.shape[0] - 1:
# not at the lowest possible edge and pixel i included
wave1 = bins[int1:int1 + 2].mean()
elif frac1 > 0.5 and int1 < bins.shape[0] - 2:
# not at the lowest possible edge and pixel i not included
wave1 = bins[int1 + 1:int1 + 3].mean()
elif frac1 == -0.5:
# at the lowest possible edge
wave1 = bins[0] - (bins[0:2].mean() - bins[0])
else: # pragma: no cover
raise exceptions.SynphotError(
'mode={0} gets unexpected frac1={1}, int1={2}'.format(
mode, frac1, int1))
# Upper end of wavelength range
if frac2 >= 0.5 and int2 < bins.shape[0] - 1:
# not out at the end and pixel i included
wave2 = bins[int2:int2 + 2].mean()
elif frac2 < 0.5 and int2 < bins.shape[0]:
# not out at end and pixel i not included
wave2 = bins[int2 - 1:int2 + 1].mean()
elif frac2 == 0.5 and int2 == bins.shape[0] - 1:
# at the very end
wave2 = bins[-1] + (bins[-1] - bins[-2:].mean())
else: # pragma: no cover
raise exceptions.SynphotError(
'mode={0} gets unexpected frac2={1}, int2={2}'.format(
mode, frac2, int2))
elif mode == 'max':
# Lower end of wavelength range
if frac1 < 0.5 and int1 < bins.shape[0]:
# not at the lowest possible edge and pixel i included
wave1 = bins[int1 - 1:int1 + 1].mean()
elif frac1 >= 0.5 and int1 < bins.shape[0] - 1:
# not at the lowest possible edge and pixel i not included
wave1 = bins[int1:int1 + 2].mean()
elif frac1 == -0.5:
# at the lowest possible edge
wave1 = bins[0] - (bins[0:2].mean() - bins[0])
else: # pragma: no cover
raise exceptions.SynphotError(
'mode={0} gets unexpected frac1={1}, int1={2}'.format(
mode, frac1, int1))
# Upper end of wavelength range
if frac2 > 0.5 and int2 < bins.shape[0] - 2:
# not out at the end and pixel i included
wave2 = bins[int2 + 1:int2 + 3].mean()
elif frac2 <= 0.5 and int2 < bins.shape[0] - 1:
# not out at end and pixel i not included
wave2 = bins[int2:int2 + 2].mean()
elif frac2 == 0.5 and int2 == bins.shape[0] - 1:
# at the very end
wave2 = bins[-1] + (bins[-1] - bins[-2:].mean())
else: # pragma: no cover
raise exceptions.SynphotError(
'mode={0} gets unexpected frac2={1}, int2={2}'.format(
mode, frac2, int2))
else: # mode == 'none'
wave1 = bins[int1] + frac1 * (bins[int1 + 1] - bins[int1])
wave2 = bins[int2] + frac2 * (bins[int2 + 1] - bins[int2])
return wave1, wave2
[docs]
def pixel_range(bins, waverange, mode='round'):
"""Calculate the number of pixels within the given wavelength range
and the given bins.
Parameters
----------
bins : array-like
Wavelengths at bin centers, each centered on a pixel.
Must be 1D array.
waverange : tuple of float
Lower and upper limits of the desired wavelength range,
in the same unit as ``bins``.
mode : {'round', 'min', 'max', 'none'}
Determines how the pixels at the edges of the wavelength range
are handled. All the options, except 'none', will return
an integer number of pixels:
* 'round' - Wavelength range edges that fall in the middle
of a pixel are counted if more than half of the pixel is
within the given wavelength range. Edges that fall in
the center of a pixel are rounded to the nearest pixel
edge. This is the default.
* 'min' - Only pixels wholly within the given wavelength
range are counted.
* 'max' - Pixels that are within the given wavelength range
by any margin are counted.
* 'none' - The exact number of encompassed pixels,
including fractional pixels, is returned.
Returns
-------
npix : number
Number of pixels.
Raises
------
synphot.exceptions.OverlapError
Given wavelength range exceeds the bounds of given bins.
synphot.exceptions.SynphotError
Invalid mode.
"""
mode = mode.lower()
if mode not in ('round', 'min', 'max', 'none'):
raise exceptions.SynphotError(
'mode={0} is invalid, must be "round", "min", "max", '
'or "none".'.format(mode))
if waverange[0] < waverange[-1]:
wave1 = waverange[0]
wave2 = waverange[-1]
else:
wave1 = waverange[-1]
wave2 = waverange[0]
# Bin values must be in ascending order.
if bins[0] > bins[-1]:
bins = bins[::-1]
# Wavelength range must be within bins
minwave = bins[0] - (bins[0:2].mean() - bins[0])
maxwave = bins[-1] + (bins[-1] - bins[-2:].mean())
if wave1 < minwave or wave2 > maxwave:
raise exceptions.OverlapError(
'Wavelength range ({0}, {1}) is out of bounds of bins '
'(min={2}, max={3}).'.format(wave1, wave2, minwave, maxwave))
if wave1 == wave2:
return 0
if mode == 'round':
ind1 = bins.searchsorted(wave1, side='right')
ind2 = bins.searchsorted(wave2, side='right')
else:
ind1 = bins.searchsorted(wave1, side='left')
ind2 = bins.searchsorted(wave2, side='left')
if mode == 'round':
npix = ind2 - ind1
elif mode == 'min':
# for ind1, figure out if pixel ind1 is wholly included or not.
# do this by figuring out where wave1 is between ind1 and ind1-1.
frac = (bins[ind1] - wave1) / (bins[ind1] - bins[ind1 - 1])
if frac < 0.5:
# ind1 is only partially included
ind1 += 1
# similar but reversed procedure for ind2
frac = (wave2 - bins[ind2 - 1]) / (bins[ind2] - bins[ind2 - 1])
if frac < 0.5:
# ind2 is only partially included
ind2 -= 1
npix = ind2 - ind1
elif mode == 'max':
# for ind1, figure out if pixel ind1-1 is partially included or not.
# do this by figuring out where wave1 is between ind1 and ind1-1.
frac = (wave1 - bins[ind1 - 1]) / (bins[ind1] - bins[ind1 - 1])
if frac < 0.5:
# ind1 is partially included
ind1 -= 1
# similar but reversed procedure for ind2
frac = (bins[ind2] - wave2) / (bins[ind2] - bins[ind2 - 1])
if frac < 0.5:
# ind2 is partially included
ind2 += 1
npix = ind2 - ind1
else: # mode == 'none'
# calculate fractional indices
frac1 = ind1 - (bins[ind1] - wave1) / (bins[ind1] - bins[ind1 - 1])
frac2 = ind2 - (bins[ind2] - wave2) / (bins[ind2] - bins[ind2 - 1])
npix = frac2 - frac1
return npix