Skip to content

Commit

Permalink
JP-3500 Create WFSS Pure-Parallel associations (spacetelescope#8528)
Browse files Browse the repository at this point in the history
  • Loading branch information
stscieisenhamer authored Jun 4, 2024
1 parent 1a62240 commit 9b074a0
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 52 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ associations
- Updated Level3 rules for new handling of NIRSpec MOS source_id formatting when
constructing output file names. [#8442]

- Create WFSS Pure-Parallel associations [#8528]

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

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
104 changes: 104 additions & 0 deletions jwst/associations/lib/rules_level2b.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
'Asn_Lv2SpecTSO',
'Asn_Lv2WFSSNIS',
'Asn_Lv2WFSSNRC',
'Asn_Lv2WFSSParallel',
'Asn_Lv2WFSC',
]

Expand Down Expand Up @@ -1034,3 +1035,106 @@ def _init_hook(self, item):

super(Asn_Lv2WFSC, self)._init_hook(item)
self.data['asn_type'] = 'wfs-image2'


@RegistryMarker.rule
class Asn_Lv2WFSSParallel(
AsnMixin_Lv2WFSS,
AsnMixin_Lv2Spectral,
):
"""Level 2b WFSS/GRISM associations for WFSS taken in pure-parallel mode
Characteristics:
- Association type: ``spec2``
- Pipeline: ``calwebb_spec2``
- Multi-object science exposures
- Single Science exposure
- Require a source catalog from processing of the corresponding direct imagery.
WFSS is executed different when taken as part of a pure-parallel proposal than when WFSS
is done as the primary. The differences are as follows. When primary, all components, the direct
image and the two GRISM exposures, are all executed within the same observation. When in parallel,
each component is taken as a separate observation.
These are always in associations of type DIRECT_IMAGE.
Another difference is that there is no ``targetid`` assigned to the parallel exposures. However, since
WFSS parallels are very specific, there is not need to constrain on target. A default value is used
for the Level 3 product naming.
"""

def __init__(self, *args, **kwargs):

self.constraints = Constraint([
DMSAttrConstraint(
name='acdirect',
sources=['asn_candidate'],
value=r"\[\('c\d{4}', 'direct_image'\)\]"
),
Constraint([
DMSAttrConstraint(
name='exp_type',
sources=['exp_type'],
value='nis_wfss|nrc_wfss',
),
DMSAttrConstraint(
name='image_exp_type',
sources=['exp_type'],
value='nis_image|nrc_image',
force_reprocess=ListCategory.NONSCIENCE,
only_on_match=True,
),
], reduce=Constraint.any),
Constraint([
SimpleConstraint(
value='science',
test=lambda value, item: self.get_exposure_type(item) != value,
force_unique=False,
),
Constraint_Single_Science(self.has_science, self.get_exposure_type),
], reduce=Constraint.any),
Constraint_Target(),
DMSAttrConstraint(
name='instrument',
sources=['instrume'],
),
])

super(Asn_Lv2WFSSParallel, self).__init__(*args, **kwargs)

@staticmethod
def find_closest_direct(science, directs):
"""Find the direct image that is closest to the science
For pure-parallel WFSS, there is only ever one direct image.
Simply return that.
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"
"""
return directs[0]

def validate_candidates(self, member):
"""Stub to always return True
For this association, stub this to always return True
Parameters
----------
member : Member
Member being added. Ignored.
Returns
-------
True
"""
return True
Loading

0 comments on commit 9b074a0

Please sign in to comment.