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

Add flux calibration #136

Merged
merged 48 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
cc049b9
Merge branch 'fix_missing_metadata' into main_develop
Jun 20, 2023
ecb208c
Added resample_gollum to EchelleSpectrum and EchelleSpectrumList to a…
Jun 21, 2023
6895c97
Bug fixes for gollum_resample.
Jun 21, 2023
b40ea61
Added transfer of meta data to a resampled list so that x_values gets…
Jun 21, 2023
d042164
Added a default available_ancillary_spectra variable to EchelleSpectr…
Jun 21, 2023
fde8930
Reverse previous change since it didn't work.
Jun 21, 2023
e7c0059
Set resampled lists and single gollum spectra to output as EchelleSpe…
Jun 21, 2023
38c23e6
Re add in line of code that was accidently deleted.
Jun 21, 2023
708c319
Minor variable name error fix.
Jun 21, 2023
2e09282
Generalize resample_gollum() to LinResample().
Jun 22, 2023
7f90837
Minor bug fix.
Jun 22, 2023
5ae12ab
Add a tutorial to explore use cases around flux calibration
gully Jun 22, 2023
eff9c33
Add slit throughput estimates (mostly for IGRINS for now).
Jun 23, 2023
c1dd597
Merge branch 'add_flux_calibration' of https://github.com/OttoStruve/…
Jun 23, 2023
a7237cd
Add ability to read in 2D IGRINS spectra.
Jun 26, 2023
ba62c02
Add function to create a slit profile from a 2D spectrum order.
Jun 26, 2023
4b21020
Moved LinResample from being a method to be a function in utilities.p…
Jun 29, 2023
d1e134d
Building up the Slit class in utilities.py. Lots of cleaning up and …
Jul 11, 2023
dde8a75
Minor bug fix.
Jul 11, 2023
85a3299
More bug fixes.
Jul 11, 2023
7be1a67
Modified apply_numpy_mask in utilities.py to be compatible with 2D sp…
Aug 17, 2023
9a41737
Major updates for IGRINS slit throughput measuring.
Aug 24, 2023
6140299
Fix IGRINS wavelengths.
Aug 24, 2023
f88c080
Added trim_overlap() to EchelleSpectrumList. This is desgined to sim…
Aug 25, 2023
e7bba34
Refined range of columns to determine IGRINS slit profile FWHM for po…
Aug 25, 2023
8e1f2d9
Add tynt and dust_extinction dependencies to the environment files.
Aug 28, 2023
3b6651d
More additions and fixes for flux calibration. More convenience func…
Sep 7, 2023
c35905d
Try changing how tynt is installed.
Sep 7, 2023
f851352
Put tynt back under plp. Not sure why it isn't installing correctly …
Sep 7, 2023
499ed25
add tynt to actions to get it to work...
gully Sep 7, 2023
30fa096
turn off numpy build matrix in GitHub actions-- it wasnt doing anythi…
gully Sep 7, 2023
795c35d
Merge branch 'add_flux_calibration' of https://github.com/OttoStruve/…
Sep 7, 2023
9419d57
Added method for HPF spectra to allow resampling of the sky fiber spe…
Sep 23, 2023
b3b1581
Allow user to scale HPF sky subtraction when using the scalar method.
Sep 23, 2023
4499827
Build the sky resampling right into the sky subtraction so nobody eve…
Sep 23, 2023
d49f9c4
Fix compatibility issue between old and new versions of the IGRINS PL…
Jan 12, 2024
979270e
Fix resample_combine_spectra.
Feb 6, 2024
00d026c
for the flux_calibration_branch
ericasaw Feb 6, 2024
e83847e
Merge branch 'add_flux_calibration' of https://github.com/OttoStruve/…
Feb 6, 2024
b189208
Update muler to read variance from .spec_a0v.fits files for outputs f…
Feb 26, 2024
346a8da
Revert to previous commit.
Feb 26, 2024
b19a0c5
:
Feb 26, 2024
18a05f5
Add compatibility with plp v3 spec_a0v.fits files.
Mar 7, 2024
96bf9d3
Add new dependencies.
Jul 18, 2024
7987499
Fix issue where uncertainity_hdus were not always there. Default is …
Jul 18, 2024
a420e4a
Fix issue where uncertainity were not always there. Default is None.
Jul 18, 2024
2e000a8
Fix typo for the word uncertainty
Jul 18, 2024
0c0cb91
Update hardcoded wavelength file in IGRINS unit tests.
Jul 19, 2024
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
4 changes: 2 additions & 2 deletions .github/workflows/muler-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ jobs:
python-version: [3.9]
specutils-version: [1.5, 1.9.1]
astropy-version: [5.2]
numpy-version: [1.18, 1.24]

#numpy-version: [1.18, 1.24]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v2
Expand Down
1 change: 1 addition & 0 deletions docs/requirements_actions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ bokeh # needed for gollum to install on RTD
gollum==0.2.1
numpydoc
sphinx-gallery
tynt
51 changes: 30 additions & 21 deletions docs/tutorials/IGRINS_SpecList_demo.ipynb

Large diffs are not rendered by default.

246 changes: 246 additions & 0 deletions docs/tutorials/flux_calibration.ipynb

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ dependencies:
- coverage
- coveralls
- twine
- dust_extinction
- pip
- pip:
- sphinx-material
- celerite2
- specutils==1.9.1
- tynt

2 changes: 2 additions & 0 deletions environment_M1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ dependencies:
- coverage
- coveralls
- twine
- dust_extinction
- pip
- pip:
- sphinx-material
- celerite2
- tynt
210 changes: 207 additions & 3 deletions src/muler/echelle.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
from scipy.signal import savgol_filter
from astropy.constants import R_jup, R_sun, G, M_jup, R_earth, c
from astropy.modeling.physical_models import BlackBody
from scipy.ndimage import median_filter, gaussian_filter1d
import specutils
from muler.utilities import apply_numpy_mask, is_list
from muler.utilities import apply_numpy_mask, is_list, resample_list


# from barycorrpy import get_BC_vel
from astropy.coordinates import SkyCoord, EarthLocation
Expand All @@ -39,6 +41,9 @@
import os
import copy

from specutils.manipulation import LinearInterpolatedResampler


from specutils.spectra.spectral_region import SpectralRegion
from specutils.analysis import equivalent_width

Expand Down Expand Up @@ -73,6 +78,7 @@ def __init__(self, *args, **kwargs):
# self.ancillary_spectra = None
super().__init__(*args, **kwargs)


@property
def snr(self):
"""The Signal-to-Noise Ratio :math:`\frac{S}{N}`, the flux divided by the uncertainty
Expand Down Expand Up @@ -699,9 +705,142 @@ def apply_boolean_mask(self, mask):
)

return spec

def get_slit_profile(self, lower=None, upper=None, slit_length=1.0):
""""For a 2D spectrum, returns the slit profile

Parameters
----------
lower : AstroPy Quantity or float
The short wavelength limit at which to define the slit profile.
If the value is a float, it assume Angstrom units.
upper : AstroPy Quantity or float
The long wavelength limit at which to define the slit profiled.
If the value is a float, it assume Angstrom units.

Returns
-------
Array with the same height as the 2D spectrum of the median estimated slit profile
"""
#Get the upper and lower wavelength limits in the correct units

assert len(np.shape(self.flux)) == 2, "Spectrum must be 2D to estimate slit profile." #Test to make sure this is a 2D spectrum

if lower is None:
lower = self.wavelength.min().value
if upper is None:
upper = self.wavelength.max().value

if type(lower) is not u.Quantity:
# Assume it's Angstroms
lower = lower * u.Angstrom
if type(upper) is not u.Quantity:
upper = upper * u.Angstrom

mask = (self.wavelength >= lower) & (self.wavelength <= upper)

flux = self.flux[:, mask].value
normalized_flux = flux / np.nansum(flux, axis=0)
median_slit_profile = np.nanmedian(normalized_flux, axis=1)

return median_slit_profile


def resample(self, target_spectrum):
"""Resample spectrum onto a new spectral_axis.
Copied from gollum.

Parameters
----------
target_spectrum : Spectrum1D
Spectrum whose wavelength grid you seek to match

Returns
-------
resampled_spec : PrecomputedSpectrum
Resampled spectrum
"""
output = LinearInterpolatedResampler()(self, target_spectrum.wavelength)

return self._copy(
spectral_axis=output.wavelength.value * output.wavelength.unit,
flux=output.flux, uncertainty=output.uncertainty, meta=self.meta,
wcs=None,
)

def instrumental_broaden(self, resolving_power=55000):
r"""Instrumentally broaden the spectrum for a given instrumental resolution
Copied verbatim from gollum.

Known limitation: If the wavelength sampling changes with wavelength,
the convolution becomes inaccurate. It may be better to FFT,
following Starfish.

Parameters
----------
resolving_power : int
Instrumental resolving power :math:`R = \frac{\lambda}{\delta \lambda}`

Returns
-------
broadened_spec : PrecomputedSpectrum
Instrumentally broadened spectrum
"""
# In detail the spectral resolution is wavelength dependent...
# For now we assume a constant resolving power
angstroms_per_pixel = np.median(np.diff(self.wavelength.angstrom))
lam0 = np.median(self.wavelength.value)
delta_lam = lam0 / resolving_power

scale_factor = 2.355
sigma = delta_lam / scale_factor / angstroms_per_pixel

convolved_flux = gaussian_filter1d(self.flux.value, sigma) * self.flux.unit
return self._copy(flux=convolved_flux)
def fill_nans(self, method=median_filter, **kwargs):
"""Fill nans with the median of surrounding pixels using
scipy.ndimage.median_filter

Parameters
----------
method: def
def to apply to smooth surrounding pixels (e.g. scipy.ndimage.median_filter)
**kwargs:
Gets passed to method (e.g. size for scipy.ndimage.median_filter)
"""
flux = self.flux
unc = self.uncertainty.array
filtered_flux = Quantity(method(flux.value, **kwargs), unit=self.flux.unit)
filtered_variance = method(unc**2, **kwargs)
filtered_unc = (filtered_variance**0.5)
found_nans = np.isnan(flux.value)
flux[found_nans] = filtered_flux[found_nans]
unc[found_nans] = filtered_unc[found_nans]

return self.__class__(
spectral_axis=self.spectral_axis, flux=flux, uncertainty=StdDevUncertainty(unc), meta=self.meta, wcs=None)
def apply(self, method=np.nansum, **kwargs):
"""
Apply any method to the spectrum. This is very general and can be used for many
things. Uncertainty is propogated.

Parameters
----------
method: def
def to apply to spectrum (e.g. np.nansum to collapse a multidimensional spectrum)
**kwargs:
Gets passed to method (e.g. axis for np.nansum)
"""
flux = self.flux
unc = self.uncertainty.array
flux = Quantity(method(self.flux.value, **kwargs), unit=self.flux.unit)
unc = method(self.uncertainty.array**2, **kwargs)**0.5
return self.__class__(
spectral_axis=self.spectral_axis, flux=flux, uncertainty=StdDevUncertainty(unc), meta=self.meta, wcs=None)

def __pow__(self, power):
"""Take flux to a power while preserving the exiting flux units.
Uuseful for airmass correction. Uncertainity is propogated by keeping the
Uuseful for airmass correction. Uncertainty is propogated by keeping the
singal-to-noise constant.

Parameters
Expand Down Expand Up @@ -778,6 +917,34 @@ def trim_edges(self, limits=None):

return spec_out

def trim_overlap(self, pivot=0.5):
"""Trim all the edges that overlap with adjacent spectra (e.g. orders)
in the list. Useful for running before stitch()."""
spec_out = copy.deepcopy(self)
n = len(spec_out)

for i in range(n): #Loop through each spectrum/order in list
#print('starting i ', i)
if i == 0: #Figure out where to trim the left side
left_limit = 0
elif self[i].spectral_axis[0] > self[i-1].spectral_axis[-1]:
left_limit = 0
else:
mid_wave = self[i].spectral_axis[0]*(1-pivot) + self[i-1].spectral_axis[-1]*(pivot)
left_limit = np.where(self[i].spectral_axis > mid_wave)[-1][0] + 1
if i == n-1: #Figure out where to trim the right side
right_limit = len(self[i].spectral_axis)
elif self[i].spectral_axis[-1] < self[i+1].spectral_axis[0]:
right_limit = len(self[i].spectral_axis)
else:
mid_wave = self[i].spectral_axis[-1]*(pivot) + self[i+1].spectral_axis[0]*(1-pivot)
right_limit = np.where(self[i].spectral_axis > mid_wave)[0][0] - 1

if left_limit > 0 or right_limit < len(self[i].spectral_axis):
spec_out[i] = spec_out[i].trim_edges((left_limit, right_limit))

return spec_out

def deblaze(self, method="spline"):
"""Remove blaze function from all orders by interpolating a spline function

Expand Down Expand Up @@ -949,7 +1116,7 @@ def __truediv__(self, other):

def __pow__(self, power):
"""Take flux to a power while preserving the exiting flux units.
Uuseful for airmass correction. Uncertainity is propagated by keeping the
Uuseful for airmass correction. Uncertainty is propagated by keeping the
singal-to-noise constant.

Parameters
Expand Down Expand Up @@ -986,3 +1153,40 @@ def flatten(self, **kwargs):
if "x_values" not in spec_out[i].meta:
spec_out[i].meta["x_values"] = self[i].meta["x_values"]
return spec_out

def fill_nans(self, method=median_filter, **kwargs):
"""Fill nans with the median of surrounding pixels using
scipy.ndimage.median_filter

Parameters
----------
method: def
def to apply to smooth surrounding pixels (e.g. scipy.ndimage.median_filter)
**kwargs:
Gets passed to method (e.g. size for scipy.ndimage.median_filter)
"""
spec_out = copy.deepcopy(self)
for i in range(len(self)):
spec_out[i] = self[i].fill_nans(method=method, **kwargs)
if "x_values" not in spec_out[i].meta:
spec_out[i].meta["x_values"] = self[i].meta["x_values"]
return spec_out

def apply(self, method=np.nansum, **kwargs):
"""
Apply any method to the spectral list. This is very general and can be used for many
things. Uncertainty is propogated.

Parameters
----------
method: def
def to apply to spectrum (e.g. np.nansum to collapse a multidimensional spectrum)
**kwargs:
Gets passed to method (e.g. axis for np.nansum)
"""
spec_out = copy.deepcopy(self)
for i in range(len(self)):
spec_out[i] = self[i].apply(method=method, **kwargs)
if "x_values" not in spec_out[i].meta:
spec_out[i].meta["x_values"] = self[i].meta["x_values"]
return spec_out
Loading
Loading