Skip to content

Commit 7b6ff7d

Browse files
Merge branch 'release/4.x' into feature/gnirsxd_tutorials
2 parents 73aa8d6 + 386f97c commit 7b6ff7d

14 files changed

Lines changed: 286 additions & 298 deletions

gemini_instruments/gnirs/lookup.py

Lines changed: 53 additions & 182 deletions
Large diffs are not rendered by default.

geminidr/gemini/parameters_irdc.py

Whitespace-only changes.

geminidr/gemini/primitives_irdc.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import numpy as np
2+
3+
from gempy.gemini import gemini_tools as gt
4+
5+
from ..gemini.primitives_gemini import Gemini
6+
from . import parameters_irdc
7+
8+
from recipe_system.utils.decorators import parameter_override, capture_provenance
9+
10+
11+
@parameter_override
12+
@capture_provenance
13+
class IRDC(Gemini):
14+
"""
15+
This is the class containing all of the preprocessing primitives
16+
for instruments that use the Gemini Infrared Detector Controller (IRDC).
17+
"""
18+
tagset = {"GEMINI"}
19+
20+
def _initialize(self, adinputs, **kwargs):
21+
super()._initialize(adinputs, **kwargs)
22+
self._param_update(parameters_irdc)
23+
24+
def nonlinearityCorrect(self, adinputs=None, suffix=None):
25+
"""
26+
Run on raw or nprepared Gemini NIRI data, this script calculates and
27+
applies a per-pixel linearity correction based on the counts in the
28+
pixel, the exposure time, the read mode, the bias level and the ROI.
29+
Pixels over the maximum correctable value are set to BADVAL unless
30+
given the force flag. Note that you may use glob expansion in infile,
31+
however, any pattern matching characters (*,?) must be either quoted
32+
or escaped with a backslash. Do we need a badval parameter that defines
33+
a value to assign to uncorrectable pixels, or do we want to just add
34+
those pixels to the DQ plane with a specific value?
35+
36+
Parameters
37+
----------
38+
suffix: str
39+
suffix to be added to output files
40+
"""
41+
# Instantiate the log
42+
log = self.log
43+
log.debug(gt.log_message("primitive", self.myself(), "starting"))
44+
timestamp_key = self.timestamp_keys[self.myself()]
45+
46+
def linearize(counts, coeffs):
47+
"""Return a linearized version of the counts in electrons per coadd"""
48+
# The coefficients are in the form of a {region: coeffs} dictionary.
49+
corrected_counts = np.empty_like(counts)
50+
for _slice, coeff in coeffs.items():
51+
log.debug("Coefficients for {} rows = {:.6f} "
52+
"{:.9e} {:.9e}".format(
53+
_slice, coeff.time_delta, coeff.gamma, coeff.eta))
54+
corrected_counts[_slice] = (
55+
counts[_slice] * (1 + counts[_slice] *
56+
(np.float32(coeff.gamma) +
57+
counts[_slice] * np.float32(coeff.eta))))
58+
return corrected_counts
59+
60+
for ad in adinputs:
61+
if ad.phu.get(timestamp_key):
62+
log.warning("No changes will be made to {}, since it has "
63+
"already been processed by nonlinearityCorrect".
64+
format(ad.filename))
65+
continue
66+
67+
total_exptime = ad.exposure_time()
68+
coadds = ad.coadds()
69+
# Check the raw exposure time (i.e., per coadd). First, convert
70+
# the total exposure time returned by the descriptor back to
71+
# the raw exposure time
72+
exptime = np.float32(total_exptime /
73+
(coadds if ad.is_coadds_summed() else 1))
74+
if exptime > 600:
75+
log.warning(f"Exposure time {exptime} for {ad.filename} is "
76+
"outside the range used to derive correction.")
77+
78+
for ext, gain, coeffs in zip(ad, ad.gain(), self._nonlinearity_coeffs(ad)):
79+
if coeffs is None:
80+
log.warning("No nonlinearity coefficients found for "
81+
f"{ad.filename} extension {ext.id} - "
82+
"no correction applied")
83+
continue
84+
elif not isinstance(coeffs, dict):
85+
# coeffs apply to entire array
86+
coeffs = {None: coeffs}
87+
88+
raw_mean_value = np.mean(ext.data) / coadds
89+
log.fullinfo("The mean value of the raw pixel data in " \
90+
"{} is {:.8f}".format(ext.filename, raw_mean_value))
91+
92+
# Create a new array that contains the corrected pixel data.
93+
# Remember that gain() returns 1.0 after ADUToElectrons
94+
raw_pixel_data = ext.data * np.float32(gain / coadds)
95+
corrected_pixel_data = (linearize(raw_pixel_data, coeffs) *
96+
np.float32(coadds / gain))
97+
98+
# Try to do something useful with the VAR plane, if it exists
99+
# Since the data are fairly pristine, VAR will simply be the
100+
# Poisson noise (divided by gain if in ADU), possibly plus RN**2
101+
# So making an additive correction will sort this out,
102+
# irrespective of whether there's read noise
103+
if ext.variance is not None:
104+
ext.variance += (corrected_pixel_data - ext.data) / np.float32(gain)
105+
# Now update the SCI extension
106+
ext.data = corrected_pixel_data
107+
108+
# Correct for the exposure time issue by scaling the counts
109+
# to the nominal exposure time
110+
time_delta = np.float32(np.mean([v.time_delta for v in coeffs.values()]))
111+
ext.multiply(exptime / (exptime + time_delta))
112+
113+
# Determine the mean of the corrected pixel data
114+
corrected_mean_value = np.mean(ext.data) / coadds
115+
log.fullinfo("The mean value of the corrected pixel data in "
116+
"{} is {:.8f}".format(ext.filename, corrected_mean_value))
117+
118+
# Correct the exposure time by adding coeff1 * coadds
119+
total_exptime = total_exptime + time_delta * coadds
120+
121+
# Update descriptors for saturation and nonlinear thresholds
122+
log.fullinfo(f"The true total exposure time = {total_exptime}")
123+
for desc in ('saturation_level', 'non_linear_level'):
124+
current_value = getattr(ext, desc)()
125+
new_value = linearize(
126+
np.array([current_value * gain / coadds]),
127+
coeffs)[0] * coadds / gain
128+
ext.hdr[ad._keyword_for(desc)] = np.round(new_value, 3)
129+
130+
# Timestamp and update filename
131+
gt.mark_history(ad, primname=self.myself(), keyword=timestamp_key)
132+
ad.update_filename(suffix=suffix, strip=True)
133+
return adinputs

geminidr/gnirs/parameters_gnirs_crossdispersed.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,3 @@ class tracePinholeAperturesConfig(parameters_spect.tracePinholeAperturesConfig):
7171
"""
7272
max_shift = config.RangeField("Maximum shift per pixel in line position",
7373
float, 0.4, min=0.001, max=0.5, inclusiveMax=True)
74-
def setDefaults(self):
75-
self.start_pos = 255

geminidr/gnirs/primitives_gnirs.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from gemini_instruments.gnirs import lookup as adlookup
99
from ..core import NearIR
10-
from ..gemini.primitives_gemini import Gemini
10+
from ..gemini.primitives_irdc import IRDC
1111
from . import parameters_gnirs
1212

1313
from recipe_system.utils.decorators import parameter_override, capture_provenance
@@ -16,11 +16,10 @@
1616
# ------------------------------------------------------------------------------
1717
@parameter_override
1818
@capture_provenance
19-
class GNIRS(Gemini, NearIR):
19+
class GNIRS(IRDC, NearIR):
2020
"""
21-
This is the class containing all of the preprocessing primitives
22-
for the F2 level of the type hierarchy tree. It inherits all
23-
the primitives from the level above
21+
This is the class containing all of the primitives used by all GNIRS
22+
modes. It inherits all the primitives from the level above.
2423
"""
2524
tagset = {"GEMINI", "GNIRS"}
2625

@@ -98,3 +97,27 @@ def standardizeInstrumentHeaders(self, adinputs=None, suffix=None):
9897
gt.mark_history(ad, primname=self.myself(), keyword=timestamp_key)
9998
ad.update_filename(suffix=suffix, strip=True)
10099
return adinputs
100+
101+
def _nonlinearity_coeffs(self, ad):
102+
"""
103+
Returns a list of namedtuples containing the necessary information to
104+
perform a nonlinearity correction. The list contains one namedtuple
105+
per extension (although normal GNIRS data only has a single extension).
106+
107+
Returns
108+
-------
109+
list of namedtuples
110+
nonlinearity info (max counts, exptime correction, gamma, eta)
111+
"""
112+
array_name = set(ad.array_name())
113+
assert len(array_name) == 1, ("Multiple array names found in {}".
114+
format(ad.filename))
115+
try:
116+
nonlin_coeffs = self.inst_adlookup.nonlin_coeffs[array_name.pop()]
117+
except KeyError:
118+
self.log.warning("No nonlinearity coefficients are available for "
119+
"this array/detector controller.")
120+
return [None] * len(ad)
121+
read_mode = ad.read_mode()
122+
well_depth = ad.well_depth_setting()
123+
return [nonlin_coeffs.get((read_mode,well_depth))] * len(ad)

geminidr/gnirs/primitives_gnirs_spect.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .primitives_gnirs import GNIRS
1414
from . import parameters_gnirs_spect
1515

16+
from gempy.gemini import gemini_tools as gt
1617
from gempy.library import wavecal
1718

1819
from recipe_system.utils.decorators import parameter_override, capture_provenance
@@ -32,6 +33,66 @@ def _initialize(self, adinputs, **kwargs):
3233
super()._initialize(adinputs, **kwargs)
3334
self._param_update(parameters_gnirs_spect)
3435

36+
def normalizeFlat(self, adinputs=None, **params):
37+
"""
38+
A GNIRS-specific primitive to normalize the spectroscopic flatfield.
39+
Because of the odd/even row effect, the trace of the brightness has
40+
two loci and can be difficult to fit, sometimes jumping from one locus
41+
to the other. This primitive will attempt to remove the odd/even
42+
offset temporarily while the fit takes place, by scaling the odd rows
43+
by a constance value to match the even rows.
44+
45+
Parameters
46+
----------
47+
suffix : str/None
48+
suffix to be added to output files
49+
center : int/None
50+
central row/column for 1D extraction (None => use middle)
51+
nsum : int
52+
number of rows/columns to average (about "center")
53+
function : str
54+
type of function to fit (splineN or polynomial types)
55+
order : int
56+
Order of the spline fit to be performed
57+
lsigma : float/None
58+
lower rejection limit in standard deviations
59+
hsigma : float/None
60+
upper rejection limit in standard deviations
61+
niter : int
62+
maximum number of rejection iterations
63+
grow : float/False
64+
growth radius for rejected pixels
65+
interactive : bool
66+
set to activate an interactive preview to fine tune the input parameters
67+
"""
68+
log = self.log
69+
log.debug(gt.log_message("primitive", self.myself(), "starting"))
70+
71+
for ad in adinputs:
72+
# XD will have multiple extensions at this point
73+
all_scaling_data = []
74+
for ext in ad:
75+
row_values = np.ma.median(np.ma.masked_array(
76+
ext.data, mask=ext.mask), axis=1)
77+
bright_enough = row_values[1::2] > 50 * ext.read_noise()
78+
if bright_enough.any():
79+
scaling = np.ma.median((row_values[::2] /
80+
row_values[1::2])[bright_enough])
81+
else:
82+
scaling = 1.0
83+
log.debug(f"Scaling for {ad.filename}:{ext.id}: {scaling:8.6f}")
84+
scaling_data = np.ones_like(ext.data)
85+
scaling_data[1::2] *= scaling
86+
ext.multiply(scaling_data)
87+
all_scaling_data.append(scaling_data)
88+
89+
# normalizeFlat() operates in-place; we don't need a return value
90+
super().normalizeFlat([ad], **params)
91+
for ext, scaling_data in zip(ad, all_scaling_data):
92+
ext.divide(scaling_data)
93+
94+
return adinputs
95+
3596
def standardizeWCS(self, adinputs=None, **params):
3697
"""
3798
This primitive updates the WCS attribute of each NDAstroData extension

geminidr/gnirs/recipes/sq/recipes_FLAT_IMAGE.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ def makeProcessedFlat(p):
2020

2121
p.prepare()
2222
p.addDQ(add_illum_mask=False)
23-
#p.nonlinearityCorrect()
2423
p.ADUToElectrons()
2524
p.addVAR(poisson_noise=True, read_noise=True)
25+
p.nonlinearityCorrect()
2626
p.makeLampFlat()
2727
p.addIllumMaskToDQ()
2828
p.normalizeFlat()

geminidr/gnirs/recipes/sq/recipes_FLAT_LS_SPECT.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def makeProcessedFlat(p):
1818
p.addDQ()
1919
p.ADUToElectrons()
2020
p.addVAR(poisson_noise=True, read_noise=True)
21+
p.nonlinearityCorrect()
2122
p.stackFlats()
2223
p.determineSlitEdges()
2324
p.maskBeyondSlit()

geminidr/gnirs/recipes/sq/recipes_FLAT_XD_SPECT.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def makeProcessedFlat(p):
2828
p.addDQ()
2929
p.ADUToElectrons()
3030
p.addVAR(poisson_noise=True, read_noise=True)
31+
p.nonlinearityCorrect()
3132
p.selectFromInputs(tags='GCAL_IR_ON,LAMPON', outstream='IRHigh')
3233
p.removeFromInputs(tags='GCAL_IR_ON,LAMPON')
3334
p.stackFlats(stream='main')

geminidr/gnirs/recipes/sq/recipes_IMAGE.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def reduce(p):
2222
p.addDQ()
2323
p.ADUToElectrons()
2424
p.addVAR(read_noise=True, poisson_noise=True)
25+
p.nonlinearityCorrect()
2526
p.darkCorrect()
2627
p.flatCorrect()
2728
p.applyDQPlane()

0 commit comments

Comments
 (0)