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

Spatial flexure reorganisation #1860

Merged
merged 14 commits into from
Oct 15, 2024
101 changes: 73 additions & 28 deletions pypeit/calibrations.py

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions pypeit/images/buildimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def construct_file_name(cls, calib_key, calib_dir=None, basename=None):

def buildimage_fromlist(spectrograph, det, frame_par, file_list, bias=None, bpm=None, dark=None,
scattlight=None, flatimages=None, maxiters=5, ignore_saturation=True,
slits=None, mosaic=None, calib_dir=None, setup=None, calib_id=None):
slits=None, mosaic=None, manual_spat_flexure=None, calib_dir=None, setup=None, calib_id=None):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add manual_spat_flexure description in the docstring

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, added 👍

"""
Perform basic image processing on a list of images and combine the results. All
core processing steps for each image are handled by :class:`~pypeit.images.rawimage.RawImage` and
Expand Down Expand Up @@ -222,6 +222,9 @@ def buildimage_fromlist(spectrograph, det, frame_par, file_list, bias=None, bpm=
Flag processed image will be a mosaic of multiple detectors. By
default, this is determined by the format of ``det`` and whether or
not this is a bias or dark frame. *Only used for testing purposes.*
manual_spat_flexure (:obj:`list`, `numpy.ndarray`_, optional):
A list of the spatial flexures for each image in file_list. This is only
used to manually correct the slit traces for spatial flexure of each image.
calib_dir (:obj:`str`, `Path`_, optional):
The directory for processed calibration files. Required for
elements of :attr:`frame_image_classes`, ignored otherwise.
Expand All @@ -247,20 +250,22 @@ def buildimage_fromlist(spectrograph, det, frame_par, file_list, bias=None, bpm=
# NOTE: This should not be necessary because FrameGroupPar explicitly
# requires frametype to be valid
msgs.error(f'{frame_par["frametype"]} is not a valid PypeIt frame type.')
manual_spatflex = manual_spat_flexure if manual_spat_flexure is not None else [np.ma.masked]*len(file_list)

# Should the detectors be reformatted into a single image mosaic?
if mosaic is None:
mosaic = isinstance(det, tuple) and frame_par['frametype'] not in ['bias', 'dark']

rawImage_list = []
# Loop on the files
for ifile in file_list:
for ii, ifile in enumerate(file_list):
# Load raw image
rawImage = rawimage.RawImage(ifile, spectrograph, det)
# Process
rawImage_list.append(rawImage.process(
frame_par['process'], scattlight=scattlight, bias=bias,
bpm=bpm, dark=dark, flatimages=flatimages, slits=slits, mosaic=mosaic))
bpm=bpm, dark=dark, flatimages=flatimages, slits=slits, mosaic=mosaic,
manual_spat_flexure=manual_spatflex[ii]))

# Do it
combineImage = combineimage.CombineImage(rawImage_list, frame_par['process'])
Expand Down
52 changes: 44 additions & 8 deletions pypeit/images/rawimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@
return np.array(rn2)

def process(self, par, bpm=None, scattlight=None, flatimages=None, bias=None, slits=None, dark=None,
mosaic=False, debug=False):
mosaic=False, manual_spat_flexure=None, debug=False):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add manual_spat_flexure description in the docstring

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also done :-)

"""
Process the data.

Expand Down Expand Up @@ -519,6 +519,9 @@
mosaic. If flats or slits are provided (and used), this *must*
be true because these objects are always defined in the mosaic
frame.
manual_spat_flexure (:obj:`float`, optional):
The spatial flexure of the image. This is only set if the user wishes to
manually correct the slit traces of this image for spatial flexure.
debug (:obj:`bool`, optional):
Run in debug mode.

Expand Down Expand Up @@ -671,9 +674,11 @@
# bias and dark subtraction) and before field flattening. Also the
# function checks that the slits exist if running the spatial flexure
# correction, so no need to do it again here.
self.spat_flexure_shift = self.spatial_flexure_shift(slits, method=self.par['spat_flexure_method'],
maxlag=self.par['spat_flexure_maxlag']) \
if self.par['spat_flexure_method'] != "skip" else None
self.spat_flexure_shift = None
if self.par['spat_flexure_method'] != "skip" or not np.ma.is_masked(manual_spat_flexure):
self.spat_flexure_shift = self.spatial_flexure_shift(slits, method=self.par['spat_flexure_method'],

Check warning on line 679 in pypeit/images/rawimage.py

View check run for this annotation

Codecov / codecov/patch

pypeit/images/rawimage.py#L679

Added line #L679 was not covered by tests
manual_spat_flexure=manual_spat_flexure,
maxlag=self.par['spat_flexure_maxlag'])

# - Subtract scattered light... this needs to be done before flatfielding.
if self.par['subtract_scattlight']:
Expand Down Expand Up @@ -766,19 +771,26 @@
return _det, self.image, self.ivar, self.datasec_img, self.det_img, self.rn2img, \
self.base_var, self.img_scale, self.bpm

def spatial_flexure_shift(self, slits, force=False, method="detector", maxlag=20):
def spatial_flexure_shift(self, slits, force=False, manual_spat_flexure=np.ma.masked, method="detector", maxlag=20):
"""
Calculate a spatial shift in the edge traces due to flexure.

This is a simple wrapper for
:func:`~pypeit.core.flexure.spat_flexure_shift`.

Args:
slits (:class:`~pypeit.slittrace.SlitTraceSet`, optional):
slits (:class:`~pypeit.slittrace.SlitTraceSet`):
Slit edge traces
force (:obj:`bool`, optional):
Force the image to be field flattened, even if the step log
(:attr:`steps`) indicates that it already has been.
manual_spat_flexure (:obj:`float`, optional):
Manually set the spatial flexure shift. If provided, this
value is used instead of calculating the shift. The default
value is `np.ma.masked`, which means the shift is calculated
from the image data. The only way this value is used is if
the user sets the `shift` parameter in their pypeit file to
be a float.
method (:obj:`str`, optional):
Method to use to calculate the spatial flexure shift. Options
are 'detector' (default), 'slit', and 'edge'. The 'detector'
Expand All @@ -801,10 +813,34 @@
if self.nimg > 1:
msgs.error('CODING ERROR: Must use a single image (single detector or detector '
'mosaic) to determine spatial flexure.')
self.spat_flexure_shift = flexure.spat_flexure_shift(self.image[0], slits, method=method, maxlag=maxlag)

# Check if the slits are provided
if slits is None:
if not np.ma.is_masked(manual_spat_flexure):
msgs.warn('Manual spatial flexure provided without slits - assuming no spatial flexure.')

Check warning on line 820 in pypeit/images/rawimage.py

View check run for this annotation

Codecov / codecov/patch

pypeit/images/rawimage.py#L818-L820

Added lines #L818 - L820 were not covered by tests
else:
msgs.warn('Cannot calculate spatial flexure without slits - assuming no spatial flexure.')
return

Check warning on line 823 in pypeit/images/rawimage.py

View check run for this annotation

Codecov / codecov/patch

pypeit/images/rawimage.py#L822-L823

Added lines #L822 - L823 were not covered by tests

# First check for manual flexure
if not np.ma.is_masked(manual_spat_flexure):
msgs.info(f'Adopting a manual spatial flexure of {manual_spat_flexure} pixels')
spat_flexure = np.full((slits.nslits, 2), np.float64(manual_spat_flexure))

Check warning on line 828 in pypeit/images/rawimage.py

View check run for this annotation

Codecov / codecov/patch

pypeit/images/rawimage.py#L826-L828

Added lines #L826 - L828 were not covered by tests
else:
spat_flexure = flexure.spat_flexure_shift(self.image[0], slits, method=method, maxlag=maxlag)

Check warning on line 830 in pypeit/images/rawimage.py

View check run for this annotation

Codecov / codecov/patch

pypeit/images/rawimage.py#L830

Added line #L830 was not covered by tests

# Print the flexure values
if np.all(spat_flexure == spat_flexure[0, 0]):
msgs.info(f'Spatial flexure is: {spat_flexure[0, 0]} pixels')

Check warning on line 834 in pypeit/images/rawimage.py

View check run for this annotation

Codecov / codecov/patch

pypeit/images/rawimage.py#L833-L834

Added lines #L833 - L834 were not covered by tests
else:
# Print the flexure values for each slit separately
for slit in range(spat_flexure.shape[0]):
msgs.info(

Check warning on line 838 in pypeit/images/rawimage.py

View check run for this annotation

Codecov / codecov/patch

pypeit/images/rawimage.py#L837-L838

Added lines #L837 - L838 were not covered by tests
f'Spatial flexure for slit {slits.spat_id[slit]} is: left={spat_flexure[slit, 0]} pixels; right={spat_flexure[slit, 1]} pixels')

self.steps[step] = True
# Return
return self.spat_flexure_shift
return spat_flexure

Check warning on line 843 in pypeit/images/rawimage.py

View check run for this annotation

Codecov / codecov/patch

pypeit/images/rawimage.py#L843

Added line #L843 was not covered by tests

def flatfield(self, flatimages, slits=None, force=False, debug=False):
"""
Expand Down
11 changes: 6 additions & 5 deletions pypeit/inputfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,20 +481,21 @@
## Build full paths to file and set frame types
data_files = []
for row in self.data:
rowkey = '' if np.ma.is_masked(row[key]) else row[key]

# Skip Empty entries?
if skip_blank and row[key].strip() in ['', 'none', 'None']:
if skip_blank and rowkey.strip() in ['', 'none', 'None']:
continue

# Skip commented out entries
if row[key].strip().startswith("#"):
if rowkey.strip().startswith("#"):
if not include_commented_out:
continue
# Strip the comment character and any whitespace following it
# from the filename
name = row[key].strip("# ")
name = rowkey.strip("# ")

Check warning on line 496 in pypeit/inputfiles.py

View check run for this annotation

Codecov / codecov/patch

pypeit/inputfiles.py#L496

Added line #L496 was not covered by tests
else:
name = row[key]
name = rowkey

# Searching..
if len(self.file_paths) > 0:
Expand All @@ -503,7 +504,7 @@
if os.path.isfile(filename):
break
else:
filename = row[key]
filename = rowkey

# Check we got a good hit
if check_exists and not os.path.isfile(filename):
Expand Down
28 changes: 25 additions & 3 deletions pypeit/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,8 +497,9 @@
_obstime = self.construct_obstime(row) if obstime is None else obstime
tiso = time.Time(_obstime, format='isot')
dtime = datetime.datetime.strptime(tiso.value, '%Y-%m-%dT%H:%M:%S.%f')
this_target = "TargetName" if np.ma.is_masked(self['target'][row]) else self['target'][row].replace(" ", "")
return '{0}-{1}_{2}_{3}{4}'.format(self['filename'][row].split('.fits')[0],
self['target'][row].replace(" ", ""),
this_target,
self.spectrograph.camera,
datetime.datetime.strftime(dtime, '%Y%m%dT'),
tiso.value.split("T")[1].replace(':',''))
Expand Down Expand Up @@ -1309,10 +1310,31 @@
Returns:
list: List of the full paths of one or more frames.
"""
if isinstance(indx, (int,np.integer)):
if isinstance(indx, (int, np.integer)):
return os.path.join(self['directory'][indx], self['filename'][indx])
return [os.path.join(d,f) for d,f in zip(self['directory'][indx], self['filename'][indx])]

def get_shifts(self, indx):
"""
Return the shifts for the provided rows.

Args:
indx (:obj:`int`, array-like):
One or more 0-indexed rows in the table with the frames
to return. Can be an array of indices or a boolean
array of the correct length.

Returns:
`numpy.ndarray`_: Array with the shifts.
"""
# Make indx an array
_indx = np.atleast_1d(indx)
# Check if shifts are defined, if not, return a masked array
if 'shift' not in self.keys():
return np.ma.array(np.zeros(_indx.shape), mask=np.ones(_indx.shape, dtype=bool))

Check warning on line 1334 in pypeit/metadata.py

View check run for this annotation

Codecov / codecov/patch

pypeit/metadata.py#L1334

Added line #L1334 was not covered by tests
# Otherwise, return the shifts
return self['shift'][indx]

def set_frame_types(self, type_bits, merge=True):
"""
Set and return a Table with the frame types and bits.
Expand Down Expand Up @@ -1574,7 +1596,7 @@

"""
if 'manual' not in self.keys():
self['manual'] = ''
self['manual'] = np.ma.array(np.zeros(len(self)), mask=np.ones(len(self), dtype=bool))
if 'shift' not in self.keys():
# Instantiate a masked array
self['shift'] = np.ma.array(np.zeros(len(self)), mask=np.ones(len(self), dtype=bool))
Expand Down
41 changes: 8 additions & 33 deletions pypeit/pypeit.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,14 +776,16 @@

# Build Science image
sci_files = self.fitstbl.frame_paths(frames)
manual_spat_flexure = self.fitstbl.get_shifts(frames)
sciImg = buildimage.buildimage_fromlist(
self.spectrograph, det, frame_par,
sci_files, bias=self.caliBrate.msbias, bpm=self.caliBrate.msbpm,
dark=self.caliBrate.msdark,
scattlight=self.caliBrate.msscattlight,
flatimages=self.caliBrate.flatimages,
slits=self.caliBrate.slits, # For flexure correction
ignore_saturation=False)
ignore_saturation=False,
manual_spat_flexure=manual_spat_flexure)

# get no bkg subtracted sciImg to generate a global sky model without bkg subtraction.
# it's a dictionary with only `image` and `ivar` keys if bkg_redux=False, otherwise it's None
Expand All @@ -795,6 +797,7 @@
bkg_redux_sciimg = sciImg
# Build the background image
bg_file_list = self.fitstbl.frame_paths(bg_frames)
bg_manual_spat_flexure = self.fitstbl.get_shifts(bg_frames)

Check warning on line 800 in pypeit/pypeit.py

View check run for this annotation

Codecov / codecov/patch

pypeit/pypeit.py#L800

Added line #L800 was not covered by tests
# TODO I think we should create a separate self.par['bkgframe'] parameter set to hold the image
# processing parameters for the background frames. This would allow the user to specify different
# parameters for the background frames than for the science frames.
Expand All @@ -805,50 +808,22 @@
scattlight=self.caliBrate.msscattlight,
flatimages=self.caliBrate.flatimages,
slits=self.caliBrate.slits,
ignore_saturation=False)
ignore_saturation=False,
manual_spat_flexure=bg_manual_spat_flexure)

# NOTE: If the spatial flexure exists for sciImg, the subtraction
# function propagates that to the subtracted image, ignoring any
# spatial flexure determined for the background image.
sciImg = bkg_redux_sciimg.sub(bgimg)

# Flexure
spat_flexure = np.zeros((self.caliBrate.slits.nslits, 2)) # No spatial flexure, unless we find it below
# use the flexure correction in the "shift" column
manual_flexure = self.fitstbl[frames[0]]['shift']
if (self.objtype == 'science' and self.par['scienceframe']['process']['spat_flexure_method'] != "skip") or \
(self.objtype == 'standard' and self.par['calibrations']['standardframe']['process']['spat_flexure_method'] != "skip") or \
not np.ma.is_masked(manual_flexure):
# First check for manual flexure
if not np.ma.is_masked(manual_flexure):
msgs.info(f'Implementing manual spatial flexure of {manual_flexure} pixels')
spat_flexure = np.full((self.caliBrate.slits.nslits, 2), np.float64(manual_flexure))
sciImg.spat_flexure = spat_flexure
else:
if sciImg.spat_flexure is not None:
msgs.info(f'Using auto-computed spatial flexure')
spat_flexure = sciImg.spat_flexure
else:
msgs.info('Assuming no spatial flexure correction')
else:
msgs.info('Assuming no spatial flexure correction')

# Print the flexure values
if np.all(spat_flexure == spat_flexure[0, 0]):
msgs.info(f'Spatial flexure is: {spat_flexure[0, 0]}')
else:
# Print the flexure values for each slit separately
for slit in range(spat_flexure.shape[0]):
msgs.info(
f'Spatial flexure for slit {self.caliBrate.slits.spat_id[slit]} is: left={spat_flexure[slit, 0]} right={spat_flexure[slit, 1]}')
# Build the initial sky mask
initial_skymask = self.load_skyregions(initial_slits=self.spectrograph.pypeline != 'SlicerIFU',
scifile=sciImg.files[0], frame=frames[0], spat_flexure=spat_flexure)
scifile=sciImg.files[0], frame=frames[0], spat_flexure=sciImg.spat_flexure)

# Deal with manual extraction
row = self.fitstbl[frames[0]]
manual_obj = ManualExtractionObj.by_fitstbl_input(
row['filename'], row['manual'], self.spectrograph) if len(row['manual'].strip()) > 0 else None
row['filename'], row['manual'], self.spectrograph) if not np.ma.is_masked(row['manual']) else None

# Instantiate Reduce object
# Required for pypeline specific object
Expand Down
10 changes: 9 additions & 1 deletion pypeit/spec2dobj.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,15 @@ def build_primary_hdr(self, raw_header, spectrograph, calib_dir=None,
# Add the spectrograph-specific sub-header
if subheader is not None:
for key in subheader.keys():
hdr[key.upper()] = subheader[key]
# Find the value and check if it is masked
if isinstance(subheader[key], (tuple, list)):
# value + comment
_value = ('', subheader[key][1]) if np.ma.is_masked(subheader[key][0]) else subheader[key]
else:
# value only
_value = '' if np.ma.is_masked(subheader[key]) else subheader[key]
# Update the header card with the corresponding value
hdr[key.upper()] = _value

# PYPEIT
# TODO Should the spectrograph be written to the header?
Expand Down
10 changes: 9 additions & 1 deletion pypeit/specobjs.py
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,15 @@ def write_to_fits(self, subheader, outfile, overwrite=True, update_det=None,
for line in str(subheader[key.upper()]).split('\n'):
header[key.upper()] = line
else:
header[key.upper()] = subheader[key]
# Find the value and check if it is masked
if isinstance(subheader[key], (tuple, list)):
# value + comment
_value = ('', subheader[key][1]) if np.ma.is_masked(subheader[key][0]) else subheader[key]
else:
# value only
_value = '' if np.ma.is_masked(subheader[key]) else subheader[key]
# Update the header card with the corresponding value
header[key.upper()] = _value
# Also store the datetime in ISOT format
if key.upper() == 'MJD':
if isinstance(subheader[key], (list, tuple)):
Expand Down
Loading