Skip to content

Commit 972a639

Browse files
Merge branch 'release/4.x' into feature/gnirsxd_tutorials
2 parents 0525a18 + 1d12e86 commit 972a639

28 files changed

Lines changed: 5621 additions & 93 deletions

gemini_instruments/gemini/adclass.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2229,4 +2229,4 @@ def actual_central_wavelength(self, *args, **kwargs):
22292229
This is not a descriptor. The args/kwargs accept whatever unit
22302230
information central_wavelength() can handle.
22312231
"""
2232-
return self.central_wavelength(*args, **kwargs)
2232+
return self.central_wavelength(*args, **kwargs)

gemini_instruments/gnirs/adclass.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ..common import build_group_id
88

99
from .lookup import detector_properties, nominal_zeropoints, read_modes
10-
from .lookup import dispersion_by_config
10+
from .lookup import dispersion_by_config, xd_orders
1111
from .lookup import pixel_scale
1212

1313
# NOTE: Temporary functions for test. gempy imports astrodata and
@@ -128,8 +128,11 @@ def dispersion(self):
128128
camera = 'Long'
129129
else:
130130
camera = None
131-
132-
filter = str(self.filter_name(pretty=True))[0]
131+
if "XD" in self.tags:
132+
order = self._grating_order()
133+
filter = xd_orders.get(order, None)
134+
else:
135+
filter = str(self.filter_name(pretty=True))[0]
133136
dispersion = dispersion_by_config.get((grating, camera), {}).get(filter)
134137

135138
if dispersion is None:

gemini_instruments/gnirs/lookup.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,13 @@
179179
("111/mm", "Short") : {"X": -0.092, "J": -0.110, "H": -0.139, "K": -0.185, "L": -0.273, "M": -0.562},
180180
("111/mm", "Long") : {"X": -0.0309, "J": -0.0371, "H": -0.0464,"K": -0.0618,"L": -0.0922,"M": -0.1875}
181181
}
182+
183+
# XD order number mapping to filter names
184+
xd_orders = {
185+
1: "L",
186+
2: "M",
187+
3: "K",
188+
4: "H",
189+
5: "J",
190+
6: "X"
191+
}

geminidr/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,4 +379,4 @@ def _inherit_params(self, params, primname, pass_suffix=False):
379379
not (k == "suffix" and not pass_suffix)}
380380

381381
class CalibrationNotFoundError(RuntimeError):
382-
pass
382+
pass

geminidr/core/parameters_spect.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,8 @@ class determineWavelengthSolutionConfig(config.core_1Dfitting_config):
202202
check=list_of_ints_check)
203203
debug_alternative_centers = config.Field("Try alternative wavelength centers?", bool, False)
204204
interactive = config.Field("Display interactive fitter?", bool, False)
205-
num_atran_lines = config.RangeField("Number of lines in ATRAN line list", int, 50.,
206-
min=10, max=300, inclusiveMax=True)
205+
num_lines = config.RangeField("Number of lines in the generated line list", int, 50.,
206+
min=10, max=1000, inclusiveMax=True)
207207
wv_band = config.ChoiceField("Water Vapor constraint", str,
208208
allowed={"20": "20%-ile",
209209
"50": "50%-ile",

geminidr/core/primitives_spect.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from geminidr import CalibrationNotFoundError
4343
from geminidr.core.primitives_resample import Resample
4444
from geminidr.gemini.lookups import DQ_definitions as DQ
45-
from geminidr.gemini.lookups import extinction_data as extinct, oh_synthetic_spectra
45+
from geminidr.gemini.lookups import extinction_data as extinct
4646
from geminidr.interactive.fit import fit1d
4747
from geminidr.interactive.fit.aperture import interactive_find_source_apertures
4848
from geminidr.interactive.fit.tracing import interactive_trace_apertures, trace_apertures_data_provider
@@ -82,7 +82,7 @@ class Spect(Resample):
8282
def _initialize(self, adinputs, **kwargs):
8383
super()._initialize(adinputs, **kwargs)
8484
self._param_update(parameters_spect)
85-
self.generated_linelist = False
85+
self.generated_linelist = None
8686

8787
def adjustWavelengthZeroPoint(self, adinputs=None, **params):
8888
"""
@@ -190,6 +190,7 @@ def _add_shift_model_to_wcs(shift, ext):
190190
"absorption": False,
191191
"debug_min_lines": 15,
192192
"debug_alternative_centers": False,
193+
"num_lines": 100,
193194
}
194195

195196
wave_scale = ext.wcs.output_frame.axes_names[0]
@@ -2394,9 +2395,9 @@ def determineWavelengthSolution(self, adinputs=None, **params):
23942395
Water vapour content (as percentile) to be used for ATRAN model
23952396
selection. If "header", then the value from the header is used.
23962397
2397-
num_atran_lines: int/None
2398+
num_lines: int/None
23982399
Maximum number of lines with largest weigths (within a wvl bin) to be
2399-
included in the generated ATRAN line list.
2400+
included in the generated line list.
24002401
24012402
debug : bool
24022403
Enable plots for debugging.
@@ -2438,7 +2439,7 @@ def determineWavelengthSolution(self, adinputs=None, **params):
24382439
log.warning(f"Cannot read file {arc_file} - "
24392440
"using default linelist")
24402441
else:
2441-
self.generated_linelist = False
2442+
self.generated_linelist = None
24422443
log.stdinfo(f"Read arc line list {arc_file}")
24432444

24442445
for ad in adinputs:
@@ -2465,9 +2466,12 @@ def determineWavelengthSolution(self, adinputs=None, **params):
24652466
config, reinit_params=["center", "nsum", "min_snr", "min_sep",
24662467
"fwidth", "central_wavelength", "dispersion",
24672468
"in_vacuo"])
2468-
if self.generated_linelist:
2469+
if self.generated_linelist is not None:
24692470
# Add some extra parameters to the UI when the linelist gets generated on-the-fly
2470-
linelist_pars = {"atran_linelist_pars": ["num_atran_lines", "resolution", "wv_band"]}
2471+
if self.generated_linelist == "atran":
2472+
linelist_pars = {"atran_linelist_pars": ["num_lines", "resolution", "wv_band"]}
2473+
elif self.generated_linelist == "airglow":
2474+
linelist_pars = {"airglow_linelist_pars": ["num_lines", "resolution"]}
24712475
uiparams.reinit_params.append(linelist_pars)
24722476

24732477
uiparams.fields["center"].max = min(

geminidr/core/primitives_telluric.py

Lines changed: 164 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
from recipe_system.utils.decorators import parameter_override, capture_provenance
2323
from geminidr.interactive.interactive import UIParameters
2424
import geminidr.interactive.server
25-
from ..gemini.lookups import qa_constraints
25+
from ..gemini.lookups import qa_constraints, airglow_synthetic_spectra
2626

2727
from gempy.library import astromodels as am, astrotools as at
28-
from gempy.library import convolution, peak_finding
28+
from gempy.library import convolution, peak_finding, wavecal
2929
from gempy.library.config import RangeField
3030
from gempy.library.calibrator import TelluricCalibrator, TelluricCorrector
3131
from gempy.library.telluric import TelluricModels, TelluricSpectrum
@@ -642,7 +642,136 @@ def _calculate_mean_pixel_shift(self, pixel_shifts):
642642
"shifts. Not shifting data.")
643643
return None
644644

645-
def _get_atran_linelist(self, wave_model=None, ext=None, config=None):
645+
def _get_airglow_linelist(self, wave_model, ext, config):
646+
"""
647+
Return a list of airglow spectral lines to be matched in the wavelength
648+
calibration, and a reference plot of a convolved synthetic spectrum,
649+
to aid the user in making the correct identifications (which is
650+
an attribute of the LineList object).
651+
652+
The spectrum is constructed within a particular wavelength
653+
range from a high-resolution list of line wavelengths and
654+
brightnesses, by convolving it to a lower spectral resolution.
655+
656+
In the region beyond 2.3um the airglow lines
657+
are virtually absent, however the atmosphere emission (T~280K) is getting
658+
stronger, and so the telluric absorption becomes prominent. Therefore
659+
in the region beyond 2.3um we use ATRAN spectrum to calculate linelist
660+
and reference spectrum, and stitch it to the airglow spectrum.
661+
662+
The linelist can be generated on-the-fly by finding peaks in the
663+
convolved spectrum, or read from disk if there exists a suitable
664+
list for this instrumental setup.
665+
666+
Parameters
667+
----------
668+
wave_model: ``astropy.modeling.models.Chebyshev1D``
669+
the current wavelength model (pixel -> wavelength), with an
670+
appropriate domain describing the illuminated region
671+
ext: single-slice ``AstroData``
672+
the extension for which a sky spectrum is being constructed
673+
config: ``config.Config`` object
674+
containing various parameters
675+
676+
Returns
677+
-------
678+
``wavecal.Linelist``
679+
list of lines to match, including data for a reference plot
680+
"""
681+
682+
log = self.log
683+
airglow_path = list(airglow_synthetic_spectra.__path__).pop()
684+
resolution = config.get("resolution") or self._get_resolution(ext)
685+
num_lines = config.get("num_lines")
686+
in_vac = config.get("in_vacuo", True)
687+
medium = 'vacuum' if in_vac else 'air'
688+
689+
# The wave_model's domain describes the illuminated region
690+
wave_model_bounds = self._wavelength_model_bounds(wave_model, ext)
691+
try:
692+
domain = wave_model.domain
693+
except AttributeError:
694+
for m in wave_model:
695+
if hasattr(m, 'domain'):
696+
domain = m.domain
697+
break
698+
else:
699+
raise ValueError("No domain in wavelength model")
700+
start_wvl, end_wvl = (np.sort(wave_model(domain)) +
701+
np.asarray(wave_model_bounds['c0']) -
702+
np.mean(wave_model_bounds['c0']))
703+
704+
dw = 0.02 * start_wvl / resolution
705+
refplot_waves = np.arange(start_wvl, end_wvl, dw, dtype=np.float32)
706+
refplot_data = np.zeros_like(refplot_waves)
707+
airglow_linelist = wavecal.LineList(os.path.join(airglow_path,
708+
"ohlist_v2.0_rev_o2_added.dat"))
709+
wlines = airglow_linelist.vacuum_wavelengths(units="nm")
710+
indices = np.logical_and(wlines > start_wvl, wlines < end_wvl)
711+
for wline, fline in zip(wlines[indices], airglow_linelist.weights[indices]):
712+
sigma = 0.42 * wline / resolution
713+
refplot_data += fline * np.exp(-0.5 * ((refplot_waves - wline) / sigma) ** 2)
714+
# Around 2300 nm is roughly where the OH lines die off and the telluric spectrum
715+
# starts dominating
716+
if end_wvl > 2314:
717+
refplot_data = refplot_data[refplot_waves < 2314]
718+
refplot_waves = refplot_waves[refplot_waves < 2314]
719+
720+
atran_data = self._get_atran_linelist(wave_model=wave_model, ext=ext,
721+
config=config, for_airglow=True)
722+
# Scale atran data to more or less match the observed spectrum and
723+
# airglow spectrum intensity
724+
refplot_data = np.concatenate((refplot_data, 3000 * atran_data[1, :]))
725+
refplot_waves = np.concatenate((refplot_waves, atran_data[0, :]))
726+
727+
refplot_spec = np.asarray([refplot_waves, refplot_data])
728+
729+
airglow_linelist = (f'airglow_linelist_{start_wvl:.0f}-{end_wvl:.0f}'
730+
f'_r{resolution:.0f}_nl{num_lines:.0f}.dat')
731+
try:
732+
linelist = LineList(os.path.join(airglow_path, airglow_linelist))
733+
log.stdinfo(f"Using generic linelist {airglow_linelist}")
734+
except FileNotFoundError:
735+
try: # prevent using previously-created linelist (for now)
736+
linelist = LineList(airglow_linelist)
737+
log.stdinfo("Using previously-created linelist in current "
738+
f"directory {airglow_linelist}")
739+
except FileNotFoundError:
740+
# We will need to create one on the fly
741+
linelist = None
742+
743+
if linelist is None:
744+
# Invert spectrum because we want the wavelengths of troughs
745+
linelist_data = make_linelist(refplot_spec,
746+
resolution=resolution,
747+
num_lines=num_lines)
748+
header = (f"Sky airglow emission line list: {start_wvl:.0f}-{end_wvl:.0f}nm\n"
749+
f"Generated by convolving the high-resolution OH linelist computed by\n"
750+
f"Rousselot et al., 2000, A & A, 354, 1134, "
751+
f"with O2 and other lines added from Oliva et al. (2015, A&A 581, A47) table 2,\n"
752+
f"and O2 lines added from Hanuschik (2003, A&A 407, 1157) table 9,\n"
753+
f" (with wavelengths transformed from air_to_vacuum using Morton (2000, ApJS, 130, 403)),\n"
754+
f" to the approximate resolution of the observation R={int(resolution)}.\n"
755+
f"The lines in the region beyond 2300nm were calculated using ATRAN synthetic spectrum \n"
756+
f"(Lord, S. D., 1992, NASA Technical Memorandum 103957)\n"
757+
"units nanometer\n"
758+
"wavelengths in VACUUM")
759+
np.savetxt(airglow_linelist, linelist_data[:, 0], fmt=['%.3f'], header=header)
760+
linelist = LineList(airglow_linelist)
761+
762+
refplot_y_axis_label = "Intensity"
763+
refplot_name = ('Synthetic spectrum of night-sky emission '
764+
f'(R={int(resolution)})')
765+
766+
refplot_data = {"refplot_spec": refplot_spec.T,
767+
"refplot_name": refplot_name,
768+
"refplot_y_axis_label": refplot_y_axis_label}
769+
770+
linelist.reference_spectrum = refplot_data
771+
return linelist
772+
773+
774+
def _get_atran_linelist(self, wave_model=None, ext=None, config=None, for_airglow=False):
646775
"""
647776
Return a list of spectral lines to be matched in the wavelength
648777
calibration, and a reference plot of a convolved synthetic spectrum,
@@ -673,6 +802,9 @@ def _get_atran_linelist(self, wave_model=None, ext=None, config=None):
673802
site = {'Gemini-North': 'mk', 'Gemini-South': 'cp'}[observatory]
674803
altitude = {'Gemini-North': 13825, 'Gemini-South': 8980}[observatory]
675804
wv_band = config.get("wv_band", "header")
805+
num_lines = config.get("num_lines")
806+
in_vac = config.get("in_vacuo", True)
807+
medium = 'vacuum' if in_vac else 'air'
676808
if wv_band == "header":
677809
wv_band = ext.raw_wv()
678810
if wv_band is None:
@@ -701,11 +833,16 @@ def _get_atran_linelist(self, wave_model=None, ext=None, config=None):
701833
start_wvl, end_wvl = (np.sort(wave_model(domain)) +
702834
np.asarray(wave_model_bounds['c0']) -
703835
np.mean(wave_model_bounds['c0']))
704-
836+
837+
# We are generating a small bit between ~2300 and 2500nm to add to the
838+
# airglow reference spectrum
839+
if for_airglow:
840+
start_wvl = 2314
841+
705842
# A linelist may be in the Gemini lookup directory, or one may
706843
# have been created in the cwd
707844
atran_linelist = (f'atran_linelist_{site}_{start_wvl:.0f}-{end_wvl:.0f}'
708-
f'_wv{wv_content:.0f}_r{resolution:.0f}.dat')
845+
f'_wv{wv_content:.0f}_r{resolution:.0f}_nl{num_lines:.0f}.dat')
709846
try:
710847
linelist = LineList(os.path.join(LOOKUPS_PATH, atran_linelist))
711848
log.stdinfo(f"Using generic linelist {atran_linelist}")
@@ -736,17 +873,25 @@ def _get_atran_linelist(self, wave_model=None, ext=None, config=None):
736873
refplot_spec = np.asarray([waves[wave_range], atran_spec],
737874
dtype=np.float32)
738875

876+
# Resampling matching the airglow spectra
877+
dw = 0.02 * start_wvl / resolution
878+
resampling = max(int(dw / sampling), 1)
879+
refplot_spec = refplot_spec[:, ::resampling]
880+
739881
# Resample the reference spectrum so it has about twice as many pixels
740882
# as the data, to avoid too much plotting overhead
741-
resampling = max(int(0.5 * atran_spec.size / np.diff(domain)[0]), 1)
742-
refplot_spec = refplot_spec[:, ::resampling]
883+
# resampling = max(int(0.5 * atran_spec.size / np.diff(domain)[0]), 1)
884+
# refplot_spec = refplot_spec[:, ::resampling]
743885

744886
refplot_spec[1] = 1 - refplot_spec[1]
887+
if for_airglow:
888+
return refplot_spec
889+
745890
if linelist is None:
746891
# Invert spectrum because we want the wavelengths of troughs
747892
linelist_data = make_linelist(refplot_spec,
748893
resolution=resolution,
749-
num_lines=config.get('num_atran_lines', 50))
894+
num_lines=config.get('num_lines', 50))
750895
header = (f"Sky emission line list: {start_wvl:.0f}-{end_wvl:.0f}nm\n"
751896
f"Generated at R={int(resolution)} from ATRAN synthetic spectrum "
752897
"(Lord, S. D., 1992, NASA Technical Memorandum 103957)\n"
@@ -845,22 +990,27 @@ def trim_peaks(peaks, weights, bin_edges, nlargest=10, sort=True):
845990

846991
# For the final line list select n // 10 peaks with largest weights
847992
# within each of 10 wavelength bins.
848-
bin_edges = np.linspace(0, flux.size + 1, num_bins + 1)
993+
994+
# atran and airglow spectra might have slightly different samplings,
995+
# so when combined in case of airglow > 2300nm, we need to use
996+
# wavelengths rather than pixel indices for bin edges
997+
bin_edges_wvl = np.linspace(wavelength[0], wavelength[-1], num_bins + 1)
998+
bin_edges = np.array([np.abs(wavelength - wvl).argmin() for wvl in bin_edges_wvl])
999+
8491000
best_pixel_peaks = trim_peaks(pixel_peaks, weights, bin_edges,
8501001
nlargest=(num_lines + num_bins - 1) // num_bins,
8511002
sort=True)
852-
8531003
# Pinpoint peak positions, and cull any peaks that couldn't be fit
8541004
# (keep_bad will return location=NaN)
855-
atran_linelist = np.vstack(peak_finding.pinpoint_peaks(
1005+
linelist = np.vstack(peak_finding.pinpoint_peaks(
8561006
flux, peaks=best_pixel_peaks[:, 0], halfwidth=2, keep_bad=True)).T
857-
atran_linelist = atran_linelist[~np.isnan(atran_linelist).any(axis=1)]
1007+
linelist = linelist[~np.isnan(linelist).any(axis=1)]
8581008

8591009
# Convert back to wavelengths
860-
atran_linelist[:, 0] = np.interp(atran_linelist[:, 0],
1010+
linelist[:, 0] = np.interp(linelist[:, 0],
8611011
np.arange(wavelength.size),
8621012
wavelength)
863-
return atran_linelist
1013+
return linelist
8641014

8651015

8661016
def find_outliers(data, sigma=3, cenfunc=np.median):

geminidr/core/tests/test_spect.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ def test_flux_conservation_consistency(astrofaker, caplog, unit,
721721
warning_given = any(record.levelname == 'WARNING' for record in caplog.records)
722722
assert warn == warning_given
723723

724-
724+
@pytest.mark.skip("This function is now part of _get_airglow_linelist() in primitives_telluric")
725725
@pytest.mark.preprocessed_data
726726
@pytest.mark.regression
727727
def test_get_sky_spectrum(path_to_inputs, path_to_refs):

0 commit comments

Comments
 (0)