|
22 | 22 | from recipe_system.utils.decorators import parameter_override, capture_provenance |
23 | 23 | from geminidr.interactive.interactive import UIParameters |
24 | 24 | import geminidr.interactive.server |
25 | | -from ..gemini.lookups import qa_constraints |
| 25 | +from ..gemini.lookups import qa_constraints, airglow_synthetic_spectra |
26 | 26 |
|
27 | 27 | 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 |
29 | 29 | from gempy.library.config import RangeField |
30 | 30 | from gempy.library.calibrator import TelluricCalibrator, TelluricCorrector |
31 | 31 | from gempy.library.telluric import TelluricModels, TelluricSpectrum |
@@ -642,7 +642,136 @@ def _calculate_mean_pixel_shift(self, pixel_shifts): |
642 | 642 | "shifts. Not shifting data.") |
643 | 643 | return None |
644 | 644 |
|
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): |
646 | 775 | """ |
647 | 776 | Return a list of spectral lines to be matched in the wavelength |
648 | 777 | 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): |
673 | 802 | site = {'Gemini-North': 'mk', 'Gemini-South': 'cp'}[observatory] |
674 | 803 | altitude = {'Gemini-North': 13825, 'Gemini-South': 8980}[observatory] |
675 | 804 | 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' |
676 | 808 | if wv_band == "header": |
677 | 809 | wv_band = ext.raw_wv() |
678 | 810 | if wv_band is None: |
@@ -701,11 +833,16 @@ def _get_atran_linelist(self, wave_model=None, ext=None, config=None): |
701 | 833 | start_wvl, end_wvl = (np.sort(wave_model(domain)) + |
702 | 834 | np.asarray(wave_model_bounds['c0']) - |
703 | 835 | 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 | + |
705 | 842 | # A linelist may be in the Gemini lookup directory, or one may |
706 | 843 | # have been created in the cwd |
707 | 844 | 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') |
709 | 846 | try: |
710 | 847 | linelist = LineList(os.path.join(LOOKUPS_PATH, atran_linelist)) |
711 | 848 | log.stdinfo(f"Using generic linelist {atran_linelist}") |
@@ -736,17 +873,25 @@ def _get_atran_linelist(self, wave_model=None, ext=None, config=None): |
736 | 873 | refplot_spec = np.asarray([waves[wave_range], atran_spec], |
737 | 874 | dtype=np.float32) |
738 | 875 |
|
| 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 | + |
739 | 881 | # Resample the reference spectrum so it has about twice as many pixels |
740 | 882 | # 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] |
743 | 885 |
|
744 | 886 | refplot_spec[1] = 1 - refplot_spec[1] |
| 887 | + if for_airglow: |
| 888 | + return refplot_spec |
| 889 | + |
745 | 890 | if linelist is None: |
746 | 891 | # Invert spectrum because we want the wavelengths of troughs |
747 | 892 | linelist_data = make_linelist(refplot_spec, |
748 | 893 | resolution=resolution, |
749 | | - num_lines=config.get('num_atran_lines', 50)) |
| 894 | + num_lines=config.get('num_lines', 50)) |
750 | 895 | header = (f"Sky emission line list: {start_wvl:.0f}-{end_wvl:.0f}nm\n" |
751 | 896 | f"Generated at R={int(resolution)} from ATRAN synthetic spectrum " |
752 | 897 | "(Lord, S. D., 1992, NASA Technical Memorandum 103957)\n" |
@@ -845,22 +990,27 @@ def trim_peaks(peaks, weights, bin_edges, nlargest=10, sort=True): |
845 | 990 |
|
846 | 991 | # For the final line list select n // 10 peaks with largest weights |
847 | 992 | # 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 | + |
849 | 1000 | best_pixel_peaks = trim_peaks(pixel_peaks, weights, bin_edges, |
850 | 1001 | nlargest=(num_lines + num_bins - 1) // num_bins, |
851 | 1002 | sort=True) |
852 | | - |
853 | 1003 | # Pinpoint peak positions, and cull any peaks that couldn't be fit |
854 | 1004 | # (keep_bad will return location=NaN) |
855 | | - atran_linelist = np.vstack(peak_finding.pinpoint_peaks( |
| 1005 | + linelist = np.vstack(peak_finding.pinpoint_peaks( |
856 | 1006 | 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)] |
858 | 1008 |
|
859 | 1009 | # Convert back to wavelengths |
860 | | - atran_linelist[:, 0] = np.interp(atran_linelist[:, 0], |
| 1010 | + linelist[:, 0] = np.interp(linelist[:, 0], |
861 | 1011 | np.arange(wavelength.size), |
862 | 1012 | wavelength) |
863 | | - return atran_linelist |
| 1013 | + return linelist |
864 | 1014 |
|
865 | 1015 |
|
866 | 1016 | def find_outliers(data, sigma=3, cenfunc=np.median): |
|
0 commit comments