Skip to content

Commit

Permalink
Merge branch 'master' into pin_numpy_build_requirement
Browse files Browse the repository at this point in the history
  • Loading branch information
zacharyburnett authored Jun 6, 2024
2 parents eb64187 + e1b66cc commit 6a30bcc
Show file tree
Hide file tree
Showing 26 changed files with 519 additions and 102 deletions.
33 changes: 32 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ assign_wcs

- Updated the routines that load NIRSpec MOS slit and source data from the MSA meta
data file to properly handle background and virtual slits, and assign appropriate
meta data to them for use downstream. [#8442]
meta data to them for use downstream. [#8442, #8533]

associations
------------
Expand All @@ -36,6 +36,16 @@ associations
- Updated Level3 rules for new handling of NIRSpec MOS source_id formatting when
constructing output file names. [#8442]

- Added default values for new non-header keywords (``MOSTILNO`` and ``DITHPTIN``)
to the default values in the ``asn_make_pool`` script. [#8508]

- Create WFSS Pure-Parallel associations [#8528]

combine_1d
----------

- Fix weights for combining errors from 1D spectra. [#8520]

dark_current
------------

Expand Down Expand Up @@ -79,6 +89,11 @@ extract_1d
- Add propagation of uncertainty when annular backgrounds are subtracted
from source spectra during IFU spectral extraction. [#8515]

- Add propagation of background uncertainty when background is subtracted from
source spectra during non-IFU spectral extraction. [#8532]

- Fix error in application of aperture correction to variance arrays. [#8530]

extract_2d
----------

Expand All @@ -103,6 +118,13 @@ general

- build with Numpy 2.0 release candidate to address ABI changes [#8527]

master_background_mos
---------------------

- Updated check for NIRSpec MOS background slits to use new naming convention:
``slit.source_name`` now contains the string "BKG" instead of
"background". [#8533]

outlier_detection
-----------------

Expand Down Expand Up @@ -217,6 +239,11 @@ tweakreg
- Change code default to use IRAF StarFinder instead of
DAO StarFinder [#8487]

- Added a check for ``(abs_)separation`` and ``(abs_)tolerance`` parameters
that ``separation`` > ``sqrt(2) * tolerance`` that will now log an error
message and skip ``tweakreg`` step when this condition is not satisfied and
source confusion is possible during catalog matching. [#8476]

wfss_contam
-----------

Expand Down Expand Up @@ -521,10 +548,14 @@ resample

- Removed any reference to the "tophat" kernel for resample step. [#8364]

- Removing unnecessary warning. Errors are propagated identically for
the 'exptime' and 'ivm' weight options. [#8258]

- Increased specificity of several warning filters. [#8320]

- Changed deprecated ``stpipe.extern.configobj`` to ``astropy.extern.configobj``. [#8320]


residual_fringe
---------------

Expand Down
6 changes: 4 additions & 2 deletions docs/jwst/tweakreg/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,8 @@ The ``tweakreg`` step has the following optional arguments:
* ``use2dhist``: A boolean indicating whether to use 2D histogram to find
initial offset. (Default=True)

* ``separation``: Minimum object separation in arcsec. (Default=1.0)
* ``separation``: Minimum object separation in arcsec. It **must be** at least
``sqrt(2)`` times larger than ``tolerance``. (Default=1.0)

* ``tolerance``: Matching tolerance for ``xyxymatch`` in arcsec. (Default=0.7)

Expand Down Expand Up @@ -374,7 +375,8 @@ Parameters used for absolute astrometry to a reference catalog.
Otherwise the initial guess for the offsets will be set to zero
(Default=True)

* ``abs_separation``: Minimum object separation in arcsec. (Default=1.0)
* ``abs_separation``: Minimum object separation in arcsec. It **must be** at
least ``sqrt(2)`` times larger than ``abs_tolerance``. (Default=1.0)

* ``abs_tolerance``: Matching tolerance for ``xyxymatch`` in arcsec.
(Default=0.7)
Expand Down
8 changes: 7 additions & 1 deletion jwst/assign_wcs/nirspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,13 @@ def get_open_msa_slits(prog_id, msa_file, msa_metadata_id, dither_position,
(s['source_name'], s['alias'], s['stellarity'], s['ra'], s['dec'])
for s in msa_source if s['source_id'] == source_id][0]
except IndexError:
log.warning("Could not retrieve source info from MSA file")
source_name = f"{prog_id}_VRT{slitlet_id}"
source_alias = "VRT{}".format(slitlet_id)
stellarity = 0.0
source_ra = 0.0
source_dec = 0.0
log.warning(f"Could not retrieve source info from MSA file; "
f"assigning virtual source_name={source_name}")

if source_id < 0:
log.info(f'Slitlet {slitlet_id} contains virtual source, with source_id={source_id}')
Expand Down
30 changes: 30 additions & 0 deletions jwst/assign_wcs/tests/test_nirspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import functools
from math import cos, sin
import os.path
import shutil

import pytest
import numpy as np
Expand Down Expand Up @@ -355,6 +356,35 @@ def test_msa_configuration_multiple_returns():
_compare_slits(slitlet_info[1], ref_slit2)


def test_msa_missing_source(tmp_path):
"""
Test the get_open_msa_slits function with missing source information.
"""
# modify an existing MSA file to remove source info
msaconfl = get_file_path('msa_configuration.fits')
bad_confl = str(tmp_path / 'bad_msa_configuration.fits')
shutil.copy(msaconfl, bad_confl)

with fits.open(bad_confl) as msa_hdu_list:
source_table = table.Table(msa_hdu_list['SOURCE_INFO'].data)
source_table.remove_rows(slice(0, -1))
msa_hdu_list['SOURCE_INFO'] = fits.table_to_hdu(source_table)
msa_hdu_list[3].name = 'SOURCE_INFO'
msa_hdu_list.writeto(bad_confl, overwrite=True)

prog_id = '1234'
msa_meta_id = 12
dither_position = 1

slitlet_info = nirspec.get_open_msa_slits(
prog_id, bad_confl, msa_meta_id, dither_position, slit_y_range=[-.5, .5])

# MSA slit: virtual source name assigned
ref_slit = trmodels.Slit(55, 9376, 1, 251, 26, -5.6, 1.0, 4, 1, '1111x',
'1234_VRT55', 'VRT55', 0.0,
-0.31716078999999997, -0.18092266, 0.0, 0.0)
_compare_slits(slitlet_info[0], ref_slit)

open_shutters = [[24], [23, 24], [22, 23, 25, 27], [22, 23, 25, 27, 28]]
main_shutter = [24, 23, 25, 28]
result = ["x", "x1", "110x01", "110101x"]
Expand Down
42 changes: 27 additions & 15 deletions jwst/associations/lib/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def check_and_set(self, item):
- List of `~jwst.associations.ProcessList`.
"""
self.matched = True
self.found_values.add(self.value)
return self.matched, []

@property
Expand Down Expand Up @@ -140,26 +141,33 @@ def copy(self):
"""Copy ourselves"""
return deepcopy(self)

def get_all_attr(self, attribute: str): # -> list[tuple[SimpleConstraint, typing.Any]]:
def get_all_attr(self, attribute, name=None):
"""Return the specified attribute
This method is meant to be overridden by classes
that need to traverse a list of constraints.
This method exists solely to support `Constraint.get_all_attr`.
This obviates the need for class/method checking.
Parameters
----------
attribute : str
The attribute to retrieve
name : str or None
Only return attribute if the name of the current constraint
matches the requested named constraints. If None, always
return value.
Returns
-------
[(self, value)] : [(SimpleConstraint, object)]
The value of the attribute in a tuple. If there is no attribute,
an empty tuple is returned.
"""
value = getattr(self, attribute)
if value is not None:
return [(self, value)]
if name is None or name == self.name:
value = getattr(self, attribute, None)
if value is not None:
if not isinstance(value, (list, set)) or len(value):
return [(self, value)]
return []

def restore(self):
Expand Down Expand Up @@ -352,6 +360,7 @@ def check_and_set(self, item):
if self.matched:
if self.force_unique:
self.value = source_value
self.found_values.add(self.value)

# Determine reprocessing
reprocess = []
Expand Down Expand Up @@ -769,17 +778,19 @@ def copy(self):
"""Copy ourselves"""
return deepcopy(self)

def get_all_attr(self, attribute: str): # -> list[tuple[typing.Union[SimpleConstraint, Constraint], typing.Any]]:
"""Return the specified attribute
This method is meant to be overridden by classes
that need to traverse a list of constraints.
def get_all_attr(self, attribute, name=None):
"""Return the specified attribute for specified constraints
Parameters
----------
attribute : str
The attribute to retrieve
name : str or None
Only return attribute if the name of the current constraint
matches the requested named constraints. If None, always
return value.
Returns
-------
result : [(SimpleConstraint or Constraint, object)[,...]]
Expand All @@ -792,11 +803,12 @@ def get_all_attr(self, attribute: str): # -> list[tuple[typing.Union[SimpleConst
If the attribute is not found.
"""
result = []
value = getattr(self, attribute)
if value is not None:
result = [(self, value)]
if name is None or name == self.name:
value = getattr(self, attribute, None)
if value is not None:
result = [(self, value)]
for constraint in self.constraints:
result.extend(constraint.get_all_attr(attribute))
result.extend(constraint.get_all_attr(attribute, name=name))

return result

Expand Down
7 changes: 6 additions & 1 deletion jwst/associations/lib/dms_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,12 @@ def _get_target(self):
The Level3 Product name representation
of the target or source ID.
"""
target_id = format_list(self.constraints['target'].found_values)
attrs = self.constraints.get_all_attr('found_values', name='target')
if attrs:
value = attrs[0][1]
else:
value = []
target_id = format_list(value)
target = 't{0:0>3s}'.format(str(target_id))
return target

Expand Down
93 changes: 62 additions & 31 deletions jwst/associations/lib/rules_level2_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1021,14 +1021,27 @@ def __init__(self, exclude_exp_types=None):
)


class Constraint_Target(DMSAttrConstraint):
class Constraint_Target(Constraint):
"""Select on target id"""

def __init__(self):
super(Constraint_Target, self).__init__(
name='target',
sources=['targetid'],
)
constraints = [
Constraint([
DMSAttrConstraint(
name='acdirect',
sources=['asn_candidate'],
value=r"\[\('c\d{4}', 'direct_image'\)\]"
),
SimpleConstraint(
name='target',
sources=lambda item: '000'
)]),
DMSAttrConstraint(
name='target',
sources=['targetid'],
)
]
super(Constraint_Target, self).__init__(constraints, reduce=Constraint.any)


# ---------------------------------------------
Expand Down Expand Up @@ -1148,32 +1161,6 @@ def add_catalog_members(self):
)
science = sciences[0]

# Get the exposure sequence for the science. Then, find
# the direct image greater than but closest to this value.
closest = directs[0] # If the search fails, just use the first.
try:
expspcin = int(getattr_from_list(science.item, ['expspcin'], _EMPTY)[1])
except KeyError:
# If exposure sequence cannot be determined, just fall through.
logger.debug('Science exposure %s has no EXPSPCIN defined.', science)
else:
min_diff = 9999 # Initialize to an invalid value.
for direct in directs:
try:
direct_expspcin = int(getattr_from_list(
direct.item, ['expspcin'], _EMPTY
)[1])
except KeyError:
# Try the next one.
logger.debug('Direct image %s has no EXPSPCIN defined.', direct)
continue
diff = direct_expspcin - expspcin
if diff < min_diff and diff > 0:
min_diff = diff
closest = direct

# Note the selected direct image. Used in `Asn_Lv2WFSS._get_opt_element`
self.direct_image = closest

# Remove all direct images from the association.
members = self.current_product['members']
Expand All @@ -1188,6 +1175,7 @@ def add_catalog_members(self):
))

# Add the Level3 catalog, direct image, and segmentation map members
self.direct_image = self.find_closest_direct(science, directs)
lv3_direct_image_root = DMS_Level3_Base._dms_product_name(self)
members.append(
Member({
Expand Down Expand Up @@ -1239,6 +1227,49 @@ def get_exposure_type(self, item, default='science'):

return exp_type

@staticmethod
def find_closest_direct(science, directs):
"""Find the direct image that is closest to the science
Closeness is defined as number difference in the exposure sequence number,
as defined in the column EXPSPCIN.
Parameters
----------
science : dict
The science member to compare against
directs : [dict[,...]]
The available direct members
Returns
-------
closest : dict
The direct image that is the "closest"
"""
closest = directs[0] # If the search fails, just use the first.
try:
expspcin = int(getattr_from_list(science.item, ['expspcin'], _EMPTY)[1])
except KeyError:
# If exposure sequence cannot be determined, just fall through.
logger.debug('Science exposure %s has no EXPSPCIN defined.', science)
else:
min_diff = 9999 # Initialize to an invalid value.
for direct in directs:
try:
direct_expspcin = int(getattr_from_list(
direct.item, ['expspcin'], _EMPTY
)[1])
except KeyError:
# Try the next one.
logger.debug('Direct image %s has no EXPSPCIN defined.', direct)
continue
diff = direct_expspcin - expspcin
if diff < min_diff and diff > 0:
min_diff = diff
closest = direct
return closest

def _get_opt_element(self):
"""Get string representation of the optical elements
Expand Down
Loading

0 comments on commit 6a30bcc

Please sign in to comment.