|
| 1 | +import numpy as np |
| 2 | +import pytest |
| 3 | +from astropy.modeling import fitting, models |
| 4 | +from specreduce.tracing import FitTrace |
| 5 | +from specreduce.utils.utils import measure_cross_dispersion_profile |
| 6 | +from specutils import Spectrum1D |
| 7 | +from astropy.nddata import NDData |
| 8 | +import astropy.units as u |
| 9 | + |
| 10 | + |
| 11 | +def mk_gaussian_img(nrows=20, ncols=16, mean=10, stddev=4): |
| 12 | + """ Makes a simple horizontal gaussian image.""" |
| 13 | + |
| 14 | + # note: this should become a fixture eventually, since other tests use |
| 15 | + # similar functions to generate test data. |
| 16 | + |
| 17 | + np.random.seed(7) |
| 18 | + col_model = models.Gaussian1D(amplitude=1, mean=mean, stddev=stddev) |
| 19 | + index_arr = np.tile(np.arange(nrows), (ncols, 1)) |
| 20 | + |
| 21 | + return col_model(index_arr.T) |
| 22 | + |
| 23 | + |
| 24 | +def mk_img_non_flat_trace(nrows=40, ncols=100, amp=10, stddev=2): |
| 25 | + """ |
| 26 | + Makes an image with a gaussian source that has a non-flat trace dispersed |
| 27 | + along the x axis. |
| 28 | + """ |
| 29 | + spec2d = np.zeros((nrows, ncols)) |
| 30 | + |
| 31 | + for ii in range(spec2d.shape[1]): |
| 32 | + mgaus = models.Gaussian1D(amplitude=amp, |
| 33 | + mean=(9.+(20/spec2d.shape[1])*ii), |
| 34 | + stddev=stddev) |
| 35 | + rg = np.arange(0, spec2d.shape[0], 1) |
| 36 | + gaus = mgaus(rg) |
| 37 | + spec2d[:, ii] = gaus |
| 38 | + |
| 39 | + return spec2d |
| 40 | + |
| 41 | + |
| 42 | +class TestMeasureCrossDispersionProfile(): |
| 43 | + |
| 44 | + @pytest.mark.parametrize('pixel', [None, 1, [1, 2, 3]]) |
| 45 | + @pytest.mark.parametrize('width', [10, 9]) |
| 46 | + def test_measure_cross_dispersion_profile(self, pixel, width): |
| 47 | + """ |
| 48 | + Basic test for `measure_cross_dispersion_profile`. Parametrized over |
| 49 | + different options for `pixel` to test using all wavelengths, a single |
| 50 | + wavelength, and a set of wavelengths, as well as different input types |
| 51 | + (plain array, quantity, Spectrum1D, and NDData), as well as `width` to |
| 52 | + use a window of all rows and a smaller window. |
| 53 | + """ |
| 54 | + |
| 55 | + # test a few input formats |
| 56 | + images = [] |
| 57 | + mean = 5.0 |
| 58 | + stddev = 4.0 |
| 59 | + dat = mk_gaussian_img(nrows=10, ncols=10, mean=mean, stddev=stddev) |
| 60 | + images.append(dat) # test unitless |
| 61 | + images.append(dat * u.DN) |
| 62 | + images.append(NDData(dat * u.DN)) |
| 63 | + images.append(Spectrum1D(flux=dat * u.DN)) |
| 64 | + |
| 65 | + for img in images: |
| 66 | + |
| 67 | + # use a flat trace at trace_pos=10, a window of width 10 around the trace |
| 68 | + # and use all 20 columns in image to create an average (median) |
| 69 | + # cross dispersion profile |
| 70 | + cdp = measure_cross_dispersion_profile(img, width=width, pixel=pixel) |
| 71 | + |
| 72 | + # make sure that if we fit a gaussian to the measured average profile, |
| 73 | + # that we get out the same profile that was used to create the image. |
| 74 | + # this should be exact since theres no noise in the image |
| 75 | + fitter = fitting.LevMarLSQFitter() |
| 76 | + mod = models.Gaussian1D() |
| 77 | + fit_model = fitter(mod, np.arange(width), cdp) |
| 78 | + |
| 79 | + assert fit_model.mean.value == np.where(cdp == max(cdp))[0][0] |
| 80 | + assert fit_model.stddev.value == stddev |
| 81 | + |
| 82 | + # test passing in a FlatTrace, and check the profile |
| 83 | + cdp = measure_cross_dispersion_profile(img, width=width, pixel=pixel) |
| 84 | + fit_model = fitter(mod, np.arange(width), cdp) |
| 85 | + assert fit_model.mean.value == np.where(cdp == max(cdp))[0][0] |
| 86 | + np.testing.assert_allclose(fit_model.stddev.value, stddev) |
| 87 | + |
| 88 | + @pytest.mark.filterwarnings("ignore:Model is linear in parameters") |
| 89 | + def test_cross_dispersion_profile_non_flat_trace(self): |
| 90 | + """ |
| 91 | + Test measure_cross_dispersion_profile with a non-flat trace. |
| 92 | + Tests with 'align_along_trace' set to both True and False, |
| 93 | + to account for the changing center of the trace and measure |
| 94 | + the true profile shape, or to 'blur' the profile, respectivley. |
| 95 | + """ |
| 96 | + |
| 97 | + image = mk_img_non_flat_trace() |
| 98 | + |
| 99 | + # fit the trace |
| 100 | + trace_fit = FitTrace(image) |
| 101 | + |
| 102 | + # when not aligning along trace and using the entire image |
| 103 | + # rows for the window, the center of the profile should follow |
| 104 | + # the shape of the trace |
| 105 | + peak_locs = [9, 10, 12, 13, 15, 16, 17, 19, 20, 22, 23, 24, 26, 27, 29] |
| 106 | + for i, pixel in enumerate(range(0, image.shape[1], 7)): |
| 107 | + profile = measure_cross_dispersion_profile(image, |
| 108 | + trace=trace_fit, |
| 109 | + width=None, |
| 110 | + pixel=pixel, |
| 111 | + align_along_trace=False, |
| 112 | + statistic='mean') |
| 113 | + peak_loc = (np.where(profile == max(profile))[0][0]) |
| 114 | + assert peak_loc == peak_locs[i] |
| 115 | + |
| 116 | + # when align_along_trace = True, the shape of the profile should |
| 117 | + # not change since (there is some wiggling around though due to the |
| 118 | + # fact that the trace is rolled to the nearest integer value. this can |
| 119 | + # be smoothed with an interpolation option later on, but it is 'rough' |
| 120 | + # for now). In this test case, the peak positions will all either |
| 121 | + # be at pixel 20 or 21. |
| 122 | + for i, pixel in enumerate(range(0, image.shape[1], 7)): |
| 123 | + profile = measure_cross_dispersion_profile(image, |
| 124 | + trace=trace_fit, |
| 125 | + width=None, |
| 126 | + pixel=pixel, |
| 127 | + align_along_trace=True, |
| 128 | + statistic='mean') |
| 129 | + peak_loc = (np.where(profile == max(profile))[0][0]) |
| 130 | + assert peak_loc in [20, 21] |
| 131 | + |
| 132 | + def test_errors_warnings(self): |
| 133 | + img = mk_gaussian_img(nrows=10, ncols=10) |
| 134 | + with pytest.raises(ValueError, |
| 135 | + match='`crossdisp_axis` must be 0 or 1'): |
| 136 | + measure_cross_dispersion_profile(img, crossdisp_axis=2) |
| 137 | + |
| 138 | + with pytest.raises(ValueError, match='`trace` must be Trace object, ' |
| 139 | + 'number to specify the location ' |
| 140 | + 'of a FlatTrace, or None to use ' |
| 141 | + 'center of image.'): |
| 142 | + measure_cross_dispersion_profile(img, trace='not a trace or a number') |
| 143 | + |
| 144 | + with pytest.raises(ValueError, match="`statistic` must be 'median' " |
| 145 | + "or 'mean'."): |
| 146 | + measure_cross_dispersion_profile(img, statistic='n/a') |
| 147 | + |
| 148 | + with pytest.raises(ValueError, match='Both `pixel` and `pixel_range` ' |
| 149 | + 'can not be set simultaneously.'): |
| 150 | + measure_cross_dispersion_profile(img, pixel=2, pixel_range=(2, 3)) |
| 151 | + |
| 152 | + with pytest.raises(ValueError, match='`pixels` must be an integer, ' |
| 153 | + 'or list of integers to specify ' |
| 154 | + 'where the crossdisperion profile ' |
| 155 | + 'should be measured.'): |
| 156 | + measure_cross_dispersion_profile(img, pixel='str') |
| 157 | + |
| 158 | + with pytest.raises(ValueError, match='`pixel_range` must be a tuple ' |
| 159 | + 'of integers.'): |
| 160 | + measure_cross_dispersion_profile(img, pixel_range=(2, 3, 5)) |
| 161 | + |
| 162 | + with pytest.raises(ValueError, match='Pixels chosen to measure cross ' |
| 163 | + 'dispersion profile are out of ' |
| 164 | + 'image bounds.'): |
| 165 | + measure_cross_dispersion_profile(img, pixel_range=(2, 12)) |
| 166 | + |
| 167 | + with pytest.raises(ValueError, match='`width` must be an integer, ' |
| 168 | + 'or None to use all ' |
| 169 | + 'cross-dispersion pixels.'): |
| 170 | + measure_cross_dispersion_profile(img, width='.') |
0 commit comments