# ExoIris: fast, flexible, and easy exoplanet transmission spectroscopy in Python.
# Copyright (C) 2024 Hannu Parviainen
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from collections.abc import Sequence
from numpy import array, floor, linspace, vstack, nan, concatenate, ndarray
[docs]
class Binning:
"""Class representing a homogeneous binning within a given range.
The `Binning` is defined by its minimum and maximum values (`xmin` and `xmax`), and either the number
of bins (`nb`), bin width (`bw`), or resolving power (`r`). If the `Binning` is
initialized giving the number of bins, the range will be divided into `nb` equally
wide bins. If the `Binning` is initialized giving the bin width, the range will be
divided into bins that are as close as `bw` as possible. Finally, if the `Binning` is
initialized with the resolving power, the bin widths aim to follow the x/xv relation.
Parameters
----------
xmin
The minimum value of the range of values to be binned.
xmax
The maximum value of the range of values to be binned.
nb
The number of bins to be used for binning the range of values.
bw
The bin width to be used for binning the range of values.
r
The resolving power to be used for binning the range of values.
Raises
------
ValueError
When none or more than one of `nb`, `bw`, and `r` are provided.
Attributes
----------
xmin
The minimum value of the range of values to be binned.
xmax
The maximum value of the range of values to be binned.
nb
The number of bins.
bw
The bin width.
r
The resolving power.
bins
An array of left and right bin edge values.
"""
[docs]
def __init__(self, xmin: float, xmax: float,
nb: float | None = None,
bw: float | None = None,
r: float | None = None) -> None:
if (nb is not None) + (bw is not None) + (r is not None) != 1:
raise ValueError(
'A binning needs to be initialized either with the number of bins (nb), bin width (bw), or resolution (r)')
self.xmin: float = xmin
self.xmax: float = xmax
self.nb: int | None = nb
self.bw: float | None = bw
self.r: float | None = r
self.bins : ndarray | None = None
"""A (nb, 2) array of left and right bin edge values."""
if r is not None:
self._bin_r()
elif bw is not None:
self._bin_bw()
else:
self._bin_nb()
def _bin_r(self) -> None:
"""Create bins for a given range using the resolution."""
bins = []
x0 = self.xmin
while True:
x1 = x0 * (2 * self.r + 1) / (2 * self.r - 1)
bins.append([x0, min(x1, self.xmax)])
x0 = x1
if x1 >= self.xmax:
break
self.bins = array(bins)
self.nb = self.bins.shape[0]
def _bin_bw(self) -> None:
"""Create bins for a given range based on the bin width `bw` and the range limits (xmin, xmax).
Raises
------
ValueError
If the bin width (bw) is greater than or equal to the binning span.
"""
if self.xmax - self.xmin <= self.bw:
raise ValueError("The bin width (bw) should be smaller than the binning span.")
self.nb = int(floor((self.xmax - self.xmin) / self.bw))
self._bin_nb()
self.nb = self.bins.shape[0]
def _bin_nb(self) -> None:
"""Create bins for a given range based on the number of bins `nb` and the range limits (xmin, xmax).
Raises
------
ValueError
If the number of bins (nb) is less than or equal to zero.
"""
if self.nb <= 0:
raise ValueError("The number of bins (nb) should be larger than zero.")
e = linspace(self.xmin, self.xmax, num=self.nb+1)
self.bins = vstack([e[:-1], e[1:]]).T
self.bw = (self.xmax - self.xmin) / self.nb
def __repr__(self) -> str:
return f"Binning {self.xmin:0.4f} - {self.xmax:.4f}: dx = {self.bw or nan:6.4f}, n = {self.nb:4d}, R = {self.r}"
def __add__(self, other: 'Binning') -> 'CompoundBinning':
if isinstance(other, Binning):
return CompoundBinning([self, other])
elif isinstance(other, CompoundBinning):
return other + self
else:
raise TypeError(f"Can't concatenate Binning and {other.__class__.__name__}")
[docs]
class CompoundBinning:
"""A class representing complex heterogeneous binning.
This class allows for creating a compound binning by combining multiple binning objects.
Parameters
----------
binnings
The list of Binning objects used for creating the bins.
Attributes
----------
binnings
List of binning objects.
bins
Array of bin edges from all binning objects.
"""
[docs]
def __init__(self, binnings: Sequence[Binning]) -> None:
self.binnings = binnings
self.bins = concatenate([b.bins for b in self.binnings])
def __repr__(self):
return 'CompoundBinning:\n' + '\n'.join([b.__repr__() for b in self.binnings])
def __add__(self, other) -> 'CompoundBinning':
if isinstance(other, CompoundBinning):
cb = CompoundBinning(self.binnings)
cb.binnings.extend(other.binnings)
cb.bins = concatenate([cb.bins, other.bins])
elif isinstance(other, Binning):
cb = CompoundBinning(self.binnings)
cb.binnings.append(other)
cb.bins = concatenate([cb.bins, other.bins])
else:
raise TypeError(f"Cannot concatenate CompoundBinning and {other.__class__.__name__}")
return cb