From a66a76fba9c51d0c63c1713638517b781393ff2a Mon Sep 17 00:00:00 2001 From: Tom Aldcroft Date: Mon, 18 Dec 2023 06:10:33 -0500 Subject: [PATCH 1/3] Factor checks out of ACAReviewTable class --- sparkles/__init__.py | 3 +- sparkles/aca_checks_table.py | 171 +++++++++ sparkles/checks.py | 515 +++++++++++++++++++++++++ sparkles/core.py | 694 ++++------------------------------ sparkles/messages.py | 36 ++ sparkles/roll_optimize.py | 5 +- sparkles/tests/test_checks.py | 236 +++++++----- 7 files changed, 938 insertions(+), 722 deletions(-) create mode 100644 sparkles/aca_checks_table.py create mode 100644 sparkles/checks.py create mode 100644 sparkles/messages.py diff --git a/sparkles/__init__.py b/sparkles/__init__.py index 63fc325..2c988cf 100644 --- a/sparkles/__init__.py +++ b/sparkles/__init__.py @@ -2,7 +2,8 @@ __version__ = ska_helpers.get_version(__package__) -from .core import ACAReviewTable, get_t_ccds_bonus, run_aca_review # noqa: F401 +from .aca_checks_table import get_t_ccds_bonus # noqa: F401 +from .core import ACAReviewTable, run_aca_review # noqa: F401 def test(*args, **kwargs): diff --git a/sparkles/aca_checks_table.py b/sparkles/aca_checks_table.py new file mode 100644 index 0000000..6230af9 --- /dev/null +++ b/sparkles/aca_checks_table.py @@ -0,0 +1,171 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +from itertools import chain + +import numpy as np +from astropy.table import Column +from chandra_aca.star_probs import guide_count +from chandra_aca.transform import yagzag_to_pixels +from proseco.catalog import ACATable +from proseco.core import MetaAttribute + +from sparkles.messages import MessagesList + +# Minimum number of "anchor stars" that are always evaluated *without* the bonus +# from dynamic background when dyn_bgd_n_faint > 0. This is mostly to avoid the +# situation where 4 stars are selected and 2 are faint bonus stars. In this case +# there would be only 2 anchor stars that ensure good tracking even without +# dyn bgd. +MIN_DYN_BGD_ANCHOR_STARS = 3 + + +def get_t_ccds_bonus(mags, t_ccd, dyn_bgd_n_faint, dyn_bgd_dt_ccd): + """Return array of t_ccds with dynamic background bonus applied. + + This adds ``dyn_bgd_dt_ccd`` to the effective CCD temperature for the + ``dyn_bgd_n_faint`` faintest stars, ensuring that at least MIN_DYN_BGD_ANCHOR_STARS + are evaluated without the bonus. See: + https://nbviewer.org/urls/cxc.harvard.edu/mta/ASPECT/ipynb/misc/guide-count-dyn-bgd.ipynb + + :param mags: array of star magnitudes + :param t_ccd: single T_ccd value (degC) + :param dyn_bgd_n_faint: number of faintest stars to apply bonus + :returns: array of t_ccds (matching ``mags``) with dynamic background bonus applied + """ + t_ccds = np.full_like(mags, t_ccd) + + # If no bonus stars then just return the input t_ccd broadcast to all stars + if dyn_bgd_n_faint == 0: + return t_ccds + + idxs = np.argsort(mags) + n_faint = min(dyn_bgd_n_faint, len(t_ccds)) + idx_bonus = max(len(t_ccds) - n_faint, MIN_DYN_BGD_ANCHOR_STARS) + for idx in idxs[idx_bonus:]: + t_ccds[idx] += dyn_bgd_dt_ccd + + return t_ccds + + +class ACAChecksTable(ACATable): + messages = MetaAttribute() + + def __init__(self, *args, **kwargs): + obsid = kwargs.pop("obsid", None) + + super().__init__(*args, **kwargs) + + self.messages = MessagesList() + + # If no data were provided then skip all the rest of the initialization. + # This happens during slicing. The result is not actually + # a functional ACAReviewTable, but it allows inspection of data. + if len(self.colnames) == 0: + return + + # Add row and col columns from yag/zag, if not already there. + self.add_row_col() + + # Input obsid could be a string repr of a number that might have have + # up to 2 decimal points. This is the case when obsid is taken from the + # ORviewer dict of ACATable pickles from prelim review. Tidy things up + # in these cases. + if obsid is not None: + f_obsid = round(float(obsid), 2) + i_obsid = int(f_obsid) + num_obsid = i_obsid if (i_obsid == f_obsid) else f_obsid + + self.obsid = num_obsid + self.acqs.obsid = num_obsid + self.guides.obsid = num_obsid + self.fids.obsid = num_obsid + + if ( + "mag_err" not in self.colnames + and self.acqs is not None + and self.guides is not None + ): + # Add 'mag_err' column after 'mag' using 'mag_err' from guides and acqs + mag_errs = { + entry["id"]: entry["mag_err"] for entry in chain(self.acqs, self.guides) + } + mag_errs = Column( + [mag_errs.get(id, 0.0) for id in self["id"]], name="mag_err" + ) + self.add_column(mag_errs, index=self.colnames.index("mag") + 1) + + # Don't want maxmag column + if "maxmag" in self.colnames: + del self["maxmag"] + + # Customizations for ACAReviewTable. Don't really need 2 decimals on these. + for name in ("yang", "zang", "row", "col"): + self._default_formats[name] = ".1f" + + # Make mag column have an extra space for catalogs with all mags < 10.0 + self._default_formats["mag"] = "5.2f" + + if self.colnames[0] != "idx": + # Move 'idx' to first column. This is really painful currently. + self.add_column(Column(self["idx"], name="idx_temp"), index=0) + del self["idx"] + self.rename_column("idx_temp", "idx") + + @property + def t_ccds_bonus(self): + """Effective T_ccd for each guide star, including dynamic background bonus.""" + if not hasattr(self, "_t_ccds_bonus"): + self._t_ccds_bonus = get_t_ccds_bonus( + self.guides["mag"], + self.guides.t_ccd, + self.dyn_bgd_n_faint, + self.dyn_bgd_dt_ccd, + ) + return self._t_ccds_bonus + + @property + def guide_count(self): + if not hasattr(self, "_guide_count"): + mags = self.guides["mag"] + self._guide_count = guide_count(mags, self.t_ccds_bonus) + return self._guide_count + + @property + def guide_count_9th(self): + if not hasattr(self, "_guide_count_9th"): + mags = self.guides["mag"] + self._guide_count_9th = guide_count(mags, self.t_ccds_bonus, count_9th=True) + return self._guide_count_9th + + @property + def acq_count(self): + if not hasattr(self, "_acq_count"): + self._acq_count = np.sum(self.acqs["p_acq"]) + return self._acq_count + + @property + def is_OR(self): + """Return ``True`` if obsid corresponds to an OR.""" + if not hasattr(self, "_is_OR"): + self._is_OR = self.obsid < 38000 + return self._is_OR + + @property + def is_ER(self): + """Return ``True`` if obsid corresponds to an ER.""" + return not self.is_OR + + @property + def att_targ(self): + if not hasattr(self, "_att_targ"): + self._att_targ = self._calc_targ_from_aca(self.att, *self.target_offset) + return self._att_targ + + def add_row_col(self): + """Add row and col columns if not present""" + if "row" in self.colnames: + return + + row, col = yagzag_to_pixels(self["yang"], self["zang"], allow_bad=True) + index = self.colnames.index("zang") + 1 + self.add_column(Column(row, name="row"), index=index) + self.add_column(Column(col, name="col"), index=index + 1) diff --git a/sparkles/checks.py b/sparkles/checks.py new file mode 100644 index 0000000..a176b98 --- /dev/null +++ b/sparkles/checks.py @@ -0,0 +1,515 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +import functools +from itertools import combinations + +import numpy as np +import proseco.characteristics as ACA +from chandra_aca.transform import mag_to_count_rate, snr_mag_for_t_ccd +from proseco.core import CatalogRow, StarTableRow + +from sparkles.aca_checks_table import ACAChecksTable +from sparkles.messages import Message + +# Observations with man_angle_next less than or equal to CREEP_AWAY_THRESHOLD +# are considered "creep away" observations. CREEP_AWAY_THRESHOLD is in units of degrees. +CREEP_AWAY_THRESHOLD = 5.0 + + +def acar_check_wrapper(func): + """Wrapper to call check functions with ACAReviewTable. + + Checks in checks are written to return a list of messages, while the checks + in sparkles.checks are written to add messages to the ACAReviewTable. This wrapper + converts the former to the latter. + """ + + @functools.wraps(func) + def wrapper(acar: ACAChecksTable, *args, **kwargs): + msgs: list[Message] = func(acar, *args, **kwargs) + messages = [ + { + key: val + for key in ("category", "text", "idx") + if (val := getattr(msg, key)) is not None + } + for msg in msgs + ] + + acar.messages.extend(messages) + + return wrapper + + +def check_guide_overlap(acar: ACAChecksTable) -> list[Message]: + """Check for overlapping tracked items. + + Overlap is defined as within 12 pixels. + """ + msgs = [] + ok = np.in1d(acar["type"], ("GUI", "BOT", "FID", "MON")) + idxs = np.flatnonzero(ok) + for idx1, idx2 in combinations(idxs, 2): + entry1 = acar[idx1] + entry2 = acar[idx2] + drow = entry1["row"] - entry2["row"] + dcol = entry1["col"] - entry2["col"] + if np.abs(drow) <= 12 and np.abs(dcol) <= 12: + msg = ( + "Overlapping track index (within 12 pix) " + f'idx [{entry1["idx"]}] and idx [{entry2["idx"]}]' + ) + msgs += [Message("critical", msg)] + return msgs + + +def check_guide_geometry(acar: ACAChecksTable) -> list[Message]: + """Check for guide stars too tightly clustered. + + (1) Check for any set of n_guide-2 stars within 500" of each other. + The nominal check here is a cluster of 3 stars within 500". For + ERs this check is very unlikely to fail. For catalogs with only + 4 guide stars this will flag for any 2 nearby stars. + + This check will likely need some refinement. + + (2) Check for all stars being within 2500" of each other. + + """ + msgs = [] + ok = np.in1d(acar["type"], ("GUI", "BOT")) + guide_idxs = np.flatnonzero(ok) + n_guide = len(guide_idxs) + + if n_guide < 2: + msg = "Cannot check geometry with fewer than 2 guide stars" + msgs += [Message("critical", msg)] + return msgs + + def dist2(g1, g2): + out = (g1["yang"] - g2["yang"]) ** 2 + (g1["zang"] - g2["zang"]) ** 2 + return out + + # First check for any set of n_guide-2 stars within 500" of each other. + min_dist = 500 + min_dist2 = min_dist**2 + for idxs in combinations(guide_idxs, n_guide - 2): + for idx0, idx1 in combinations(idxs, 2): + # If any distance in this combination exceeds min_dist then + # the combination is OK. + if dist2(acar[idx0], acar[idx1]) > min_dist2: + break + else: + # Every distance was too small, issue a warning. + cat_idxs = [idx + 1 for idx in idxs] + msg = f'Guide indexes {cat_idxs} clustered within {min_dist}" radius' + + if acar.man_angle_next > CREEP_AWAY_THRESHOLD: + msg += f" (man_angle_next > {CREEP_AWAY_THRESHOLD})" + msgs += [Message("critical", msg)] + else: + msg += f" (man_angle_next <= {CREEP_AWAY_THRESHOLD})" + msgs += [Message("warning", msg)] + + # Check for all stars within 2500" of each other + min_dist = 2500 + min_dist2 = min_dist**2 + for idx0, idx1 in combinations(guide_idxs, 2): + if dist2(acar[idx0], acar[idx1]) > min_dist2: + break + else: + msg = f'Guide stars all clustered within {min_dist}" radius' + msgs += [Message("warning", msg)] + return msgs + + +def check_guide_fid_position_on_ccd( + acar: ACAChecksTable, entry: CatalogRow +) -> list[Message]: + """Check position of guide stars and fid lights on CCD.""" + msgs = [] + # Shortcuts and translate y/z to yaw/pitch + dither_guide_y = acar.dither_guide.y + dither_guide_p = acar.dither_guide.z + + # Set "dither" for FID to be pseudodither of 5.0 to give 1 pix margin + # Set "track phase" dither for BOT GUI to max guide dither over + # interval or 20.0 if undefined. TO DO: hand the guide guide dither + dither_track_y = 5.0 if (entry["type"] == "FID") else dither_guide_y + dither_track_p = 5.0 if (entry["type"] == "FID") else dither_guide_p + + row_lim = ACA.max_ccd_row - ACA.CCD["window_pad"] + col_lim = ACA.max_ccd_col - ACA.CCD["window_pad"] + + def sign(axis): + """Return sign of the corresponding entry value. + + Note that np.sign returns 0 if the value is 0.0, not the right thing here. + """ + return -1 if (entry[axis] < 0) else 1 + + track_lims = { + "row": (row_lim - dither_track_y * ACA.ARC_2_PIX) * sign("row"), + "col": (col_lim - dither_track_p * ACA.ARC_2_PIX) * sign("col"), + } + + for axis in ("row", "col"): + track_delta = abs(track_lims[axis]) - abs(entry[axis]) + track_delta = np.round( + track_delta, decimals=1 + ) # Official check is to 1 decimal + for delta_lim, category in ((3.0, "critical"), (5.0, "info")): + if track_delta < delta_lim: + text = ( + f"Less than {delta_lim} pix edge margin {axis} " + f"lim {track_lims[axis]:.1f} " + f"val {entry[axis]:.1f} " + f"delta {track_delta:.1f}" + ) + msgs += [Message(category, text, idx=entry["idx"])] + break + return msgs + + +# TO DO: acq star position check: +# For acq stars, the distance to the row/col padded limits are also confirmed, +# but code to track which boundary is exceeded (row or column) is not present. +# Note from above that the pix_row_pad used for row_lim has 7 more pixels of padding +# than the pix_col_pad used to determine col_lim. +# acq_edge_delta = min((row_lim - dither_acq_y / ang_per_pix) - abs(pixel_row), +# (col_lim - dither_acq_p / ang_per_pix) - abs(pixel_col)) +# if ((entry_type =~ /BOT|ACQ/) and (acq_edge_delta < (-1 * 12))){ +# push @orange_warn, sprintf "alarm [%2d] Acq Off (padded) CCD by > 60 arcsec.\n",i +# } +# elsif ((entry_type =~ /BOT|ACQ/) and (acq_edge_delta < 0)){ +# push @{acar->{fyi}}, +# sprintf "alarm [%2d] Acq Off (padded) CCD (P_ACQ should be < .5)\n",i +# } + + +def check_acq_p2(acar: ACAChecksTable) -> list[Message]: + """Check acquisition catalog safing probability.""" + msgs = [] + P2 = -np.log10(acar.acqs.calc_p_safe()) + P2 = np.round(P2, decimals=2) # Official check is to 2 decimals + obs_type = "OR" if acar.is_OR else "ER" + P2_lim = 2.0 if acar.is_OR else 3.0 + if P2 < P2_lim: + msgs += [Message("critical", f"P2: {P2:.2f} less than {P2_lim} for {obs_type}")] + elif P2 < P2_lim + 1: + msgs += [ + Message("warning", f"P2: {P2:.2f} less than {P2_lim + 1} for {obs_type}") + ] + return msgs + + +def check_include_exclude(acar: ACAChecksTable) -> list[Message]: + """Check for included or excluded guide or acq stars or fids (info)""" + msgs = [] + call_args = acar.call_args + for typ in ("acq", "guide", "fid"): + for action in ("include", "exclude"): + ids = call_args.get(f"{action}_ids_{typ}") + if ids is not None: + msg = f"{action}d {typ} ID(s): {ids}" + + # Check for halfwidths. This really only applies to + # include_halfws_acq, but having it here in the loop doesn't hurt. + halfws = call_args.get(f"{action}_halfws_{typ}") + if halfws is not None: + msg = msg + f" halfwidths(s): {halfws}" + + msgs += [Message("info", msg)] + return msgs + + +def check_guide_count(acar: ACAChecksTable) -> list[Message]: + """ + Check for sufficient guide star fractional count. + + Also check for multiple very-bright stars + + """ + msgs = [] + obs_type = "ER" if acar.is_ER else "OR" + count_9th_lim = 3.0 + if acar.is_ER and np.round(acar.guide_count_9th, decimals=2) < count_9th_lim: + # Determine the threshold 9th mag equivalent value at the effective guide t_ccd + mag9 = snr_mag_for_t_ccd(acar.guides.t_ccd, 9.0, -10.9) + msgs += [ + Message( + "critical", + ( + f"{obs_type} count of 9th ({mag9:.1f} for {acar.guides.t_ccd:.1f}C)" + f" mag guide stars {acar.guide_count_9th:.2f} < {count_9th_lim}" + ), + ) + ] + + # Rounded guide count + guide_count_round = np.round(acar.guide_count, decimals=2) + + # Set critical guide_count threshold + # For observations with creep-away in place as a mitigation for end of observation + # roll error, we can accept a lower guide_count (3.5 instead of 4.0). + # See https://occweb.cfa.harvard.edu/twiki/bin/view/Aspect/StarWorkingGroupMeeting2023x03x15 + if acar.is_OR: + count_lim = 3.5 if (acar.man_angle_next <= CREEP_AWAY_THRESHOLD) else 4.0 + else: + count_lim = 6.0 + + if guide_count_round < count_lim: + msgs += [ + Message( + "critical", + f"{obs_type} count of guide stars {acar.guide_count:.2f} < {count_lim}", + ) + ] + # If in the 3.5 to 4.0 range, this probably deserves a warning. + elif count_lim == 3.5 and guide_count_round < 4.0: + msgs += [ + Message( + "warning", + f"{obs_type} count of guide stars {acar.guide_count:.2f} < 4.0", + ) + ] + + bright_cnt_lim = 1 if acar.is_OR else 3 + if np.count_nonzero(acar.guides["mag"] < 5.5) > bright_cnt_lim: + msgs += [ + Message( + "caution", + f"{obs_type} with more than {bright_cnt_lim} stars brighter than 5.5.", + ) + ] + + # Requested slots for guide stars and mon windows + n_guide_or_mon_request = acar.call_args["n_guide"] + + # Actual guide stars + n_guide = len(acar.guides) + + # Actual mon windows. For catalogs from pickles from proseco < 5.0 + # acar.mons might be initialized to a NoneType or not be an attribute so + # handle that as 0 monitor windows. + try: + n_mon = len(acar.mons) + except (TypeError, AttributeError): + n_mon = 0 + + # Different number of guide stars than requested + if n_guide + n_mon != n_guide_or_mon_request: + if n_mon == 0: + # Usual case + msg = ( + f"{obs_type} with {n_guide} guides " + f"but {n_guide_or_mon_request} were requested" + ) + else: + msg = ( + f"{obs_type} with {n_guide} guides and {n_mon} monitor(s) " + f"but {n_guide_or_mon_request} guides or mon slots were requested" + ) + msgs += [Message("caution", msg)] + + # Caution for any "unusual" guide star request + typical_n_guide = 5 if acar.is_OR else 8 + if n_guide_or_mon_request != typical_n_guide: + or_mon_slots = " or mon slots" if n_mon > 0 else "" + msg = ( + f"{obs_type} with" + f" {n_guide_or_mon_request} guides{or_mon_slots} requested but" + f" {typical_n_guide} is typical" + ) + msgs += [Message("caution", msg)] + + return msgs + + +# Add a check that for ORs with guide count between 3.5 and 4.0, the +# dither is 4 arcsec if dynamic background not enabled. +def check_dither(acar: ACAChecksTable) -> list[Message]: + """Check dither. + + This presently checks that dither is 4x4 arcsec if dynamic background is not in + use and the field has a low guide_count. + """ + msgs = [] + # Skip check if guide_count is 4.0 or greater + if acar.guide_count >= 4.0: + return msgs + + # Skip check if dynamic backround is enabled (inferred from dyn_bgd_n_faint) + if acar.dyn_bgd_n_faint > 0: + return msgs + + # Check that dither is <= 4x4 arcsec + if acar.dither_guide.y > 4.0 or acar.dither_guide.z > 4.0: + msgs += [ + Message( + "critical", + f"guide_count {acar.guide_count:.2f} and dither > 4x4 arcsec", + ) + ] + + return msgs + + +def check_pos_err_guide(acar: ACAChecksTable, star: StarTableRow) -> list[Message]: + """Warn on stars with larger POS_ERR (warning at 1" critical at 2")""" + msgs = [] + agasc_id = star["id"] + idx = acar.get_id(agasc_id)["idx"] + # POS_ERR is in milliarcsecs in the table + pos_err = star["POS_ERR"] * 0.001 + for limit, category in ((2.0, "critical"), (1.25, "warning")): + if np.round(pos_err, decimals=2) > limit: + msgs += [ + Message( + category, + ( + f"Guide star {agasc_id} POS_ERR {pos_err:.2f}, limit" + f" {limit} arcsec" + ), + idx=idx, + ) + ] + break + return msgs + + +def check_imposters_guide(acar: ACAChecksTable, star: StarTableRow) -> list[Message]: + """Warn on stars with larger imposter centroid offsets""" + + # Borrow the imposter offset method from starcheck + def imposter_offset(cand_mag, imposter_mag): + """Get imposter offset. + + For a given candidate star and the pseudomagnitude of the brightest 2x2 + imposter calculate the max offset of the imposter counts are at the edge of + the 6x6 (as if they were in one pixel). This is somewhat the inverse of + proseco.get_pixmag_for_offset. + """ + cand_counts = mag_to_count_rate(cand_mag) + spoil_counts = mag_to_count_rate(imposter_mag) + return spoil_counts * 3 * 5 / (spoil_counts + cand_counts) + + msgs = [] + agasc_id = star["id"] + idx = acar.get_id(agasc_id)["idx"] + offset = imposter_offset(star["mag"], star["imp_mag"]) + for limit, category in ((4.0, "critical"), (2.5, "warning")): + if np.round(offset, decimals=1) > limit: + msgs += [ + Message( + category, + f"Guide star imposter offset {offset:.1f}, limit {limit} arcsec", + idx=idx, + ) + ] + break + return msgs + + +def check_guide_is_candidate(acar: ACAChecksTable, star: StarTableRow) -> list[Message]: + """Critical for guide star that is not a valid guide candidate. + + This can occur for a manually included guide star. In rare cases + the star may still be acceptable and ACA review can accept the warning. + """ + msgs = [] + if not acar.guides.get_candidates_mask(star): + agasc_id = star["id"] + idx = acar.get_id(agasc_id)["idx"] + msgs += [ + Message( + "critical", + f"Guide star {agasc_id} does not meet guide candidate criteria", + idx=idx, + ) + ] + return msgs + + +def check_too_bright_guide(acar: ACAChecksTable, star: StarTableRow) -> list[Message]: + """Warn on guide stars that may be too bright. + + - Critical if within 2 * mag_err of the hard 5.2 limit, caution within 3 * mag_err + + """ + msgs = [] + agasc_id = star["id"] + idx = acar.get_id(agasc_id)["idx"] + mag_err = star["mag_err"] + for mult, category in ((2, "critical"), (3, "caution")): + if star["mag"] - (mult * mag_err) < 5.2: + msgs += [ + Message( + category, + ( + f"Guide star {agasc_id} within {mult}*mag_err of 5.2 " + f"(mag_err={mag_err:.2f})" + ), + idx=idx, + ) + ] + break + return msgs + + +def check_bad_stars(entry: CatalogRow) -> list[Message]: + """Check if entry (guide or acq) is in bad star set from proseco + + :param entry: ACATable row + :return: None + """ + msgs = [] + if entry["id"] in ACA.bad_star_set: + msg = f'Star {entry["id"]} is in proseco bad star set' + msgs += [Message("critical", msg, idx=entry["idx"])] + return msgs + + +def check_fid_spoiler_score(idx, fid) -> list[Message]: + """ + Check the spoiler warnings for fid + + :param idx: catalog index of fid entry being checked + :param fid: corresponding row of ``fids`` table + :return: None + """ + msgs = [] + if fid["spoiler_score"] == 0: + return msgs + + fid_id = fid["id"] + category_map = {"yellow": "warning", "red": "critical"} + + for spoiler in fid["spoilers"]: + msg = ( + f'Fid {fid_id} has {spoiler["warn"]} spoiler: star {spoiler["id"]} with' + f' mag {spoiler["mag"]:.2f}' + ) + msgs += [Message(category_map[spoiler["warn"]], msg, idx=idx)] + return msgs + + +def check_fid_count(acar: ACAChecksTable) -> list[Message]: + """ + Check for the correct number of fids. + + :return: None + """ + msgs = [] + obs_type = "ER" if acar.is_ER else "OR" + + if len(acar.fids) != acar.n_fid: + msg = f"{obs_type} has {len(acar.fids)} fids but {acar.n_fid} were requested" + msgs += [Message("critical", msg)] + + # Check for "typical" number of fids for an OR / ER (3 or 0) + typical_n_fid = 3 if acar.is_OR else 0 + if acar.n_fid != typical_n_fid: + msg = f"{obs_type} requested {acar.n_fid} fids but {typical_n_fid} is typical" + msgs += [Message("caution", msg)] + + return msgs diff --git a/sparkles/core.py b/sparkles/core.py index 6f2600d..6ab89e3 100644 --- a/sparkles/core.py +++ b/sparkles/core.py @@ -9,21 +9,19 @@ import pprint import re import traceback -from itertools import chain, combinations from pathlib import Path import chandra_aca import numpy as np import proseco -import proseco.characteristics as ACA -from astropy.table import Column, Table -from chandra_aca.star_probs import guide_count -from chandra_aca.transform import mag_to_count_rate, snr_mag_for_t_ccd, yagzag_to_pixels +from astropy.table import Table from jinja2 import Template -from proseco.catalog import ACATable from proseco.core import MetaAttribute -from .roll_optimize import RollOptimizeMixin +from sparkles import checks +from sparkles.aca_checks_table import ACAChecksTable +from sparkles.messages import Message, MessagesList +from sparkles.roll_optimize import RollOptimizeMixin CACHE = {} FILEDIR = Path(__file__).parent @@ -35,38 +33,6 @@ # dyn bgd. MIN_DYN_BGD_ANCHOR_STARS = 3 -# Observations with man_angle_next less than or equal to CREEP_AWAY_THRESHOLD -# are considered "creep away" observations. CREEP_AWAY_THRESHOLD is in units of degrees. -CREEP_AWAY_THRESHOLD = 5.0 - - -def get_t_ccds_bonus(mags, t_ccd, dyn_bgd_n_faint, dyn_bgd_dt_ccd): - """Return array of t_ccds with dynamic background bonus applied. - - This adds ``dyn_bgd_dt_ccd`` to the effective CCD temperature for the - ``dyn_bgd_n_faint`` faintest stars, ensuring that at least MIN_DYN_BGD_ANCHOR_STARS - are evaluated without the bonus. See: - https://nbviewer.org/urls/cxc.harvard.edu/mta/ASPECT/ipynb/misc/guide-count-dyn-bgd.ipynb - - :param mags: array of star magnitudes - :param t_ccd: single T_ccd value (degC) - :param dyn_bgd_n_faint: number of faintest stars to apply bonus - :returns: array of t_ccds (matching ``mags``) with dynamic background bonus applied - """ - t_ccds = np.full_like(mags, t_ccd) - - # If no bonus stars then just return the input t_ccd broadcast to all stars - if dyn_bgd_n_faint == 0: - return t_ccds - - idxs = np.argsort(mags) - n_faint = min(dyn_bgd_n_faint, len(t_ccds)) - idx_bonus = max(len(t_ccds) - n_faint, MIN_DYN_BGD_ANCHOR_STARS) - for idx in idxs[idx_bonus:]: - t_ccds[idx] += dyn_bgd_dt_ccd - - return t_ccds - def main(sys_args=None): """Command line interface to preview_load()""" @@ -365,7 +331,7 @@ def _run_aca_review( aca.dyn_bgd_n_faint = dyn_bgd_n_faint aca.guides.dyn_bgd_n_faint = dyn_bgd_n_faint - aca.check_catalog() + check_catalog(aca) # Find roll options if requested if roll_level == "all" or aca.messages >= roll_level: @@ -602,33 +568,11 @@ def get_summary_text(acas): return "\n".join(lines) -class MessagesList(list): - categories = ("all", "info", "caution", "warning", "critical", "none") - - def __eq__(self, other): - if isinstance(other, str): - return [msg for msg in self if msg["category"] == other] - else: - return super().__eq__(other) - - def __ge__(self, other): - if isinstance(other, str): - other_idx = self.categories.index(other) - return [ - msg - for msg in self - if self.categories.index(msg["category"]) >= other_idx - ] - else: - return super().__ge__(other) - - -class ACAReviewTable(ACATable, RollOptimizeMixin): +class ACAReviewTable(ACAChecksTable, RollOptimizeMixin): # Whether this instance is a roll option (controls how HTML report page is formatted) is_roll_option = MetaAttribute() roll_options = MetaAttribute() roll_info = MetaAttribute() - messages = MetaAttribute() def __init__(self, *args, **kwargs): """Init review methods and attrs in ``aca`` object *in-place*. @@ -644,7 +588,6 @@ def __init__(self, *args, **kwargs): # if data is None: # raise ValueError(f'data arg must be set to initialize {self.__class__.__name__}') - obsid = kwargs.pop("obsid", None) loud = kwargs.pop("loud", False) is_roll_option = kwargs.pop("is_roll_option", False) @@ -664,11 +607,6 @@ def __init__(self, *args, **kwargs): self.roll_options = None self.roll_info = None - # Add row and col columns from yag/zag, if not already there. - self.add_row_col() - - self.messages = MessagesList() # Warning messages - # Instance attributes that won't survive pickling self.context = {} # Jinja2 context for output HTML review self.loud = loud @@ -676,83 +614,6 @@ def __init__(self, *args, **kwargs): self.obsid_dir = None self.roll_options_table = None - # Input obsid could be a string repr of a number that might have have - # up to 2 decimal points. This is the case when obsid is taken from the - # ORviewer dict of ACATable pickles from prelim review. Tidy things up - # in these cases. - if obsid is not None: - f_obsid = round(float(obsid), 2) - i_obsid = int(f_obsid) - num_obsid = i_obsid if (i_obsid == f_obsid) else f_obsid - - self.obsid = num_obsid - self.acqs.obsid = num_obsid - self.guides.obsid = num_obsid - self.fids.obsid = num_obsid - - if ( - "mag_err" not in self.colnames - and self.acqs is not None - and self.guides is not None - ): - # Add 'mag_err' column after 'mag' using 'mag_err' from guides and acqs - mag_errs = { - entry["id"]: entry["mag_err"] for entry in chain(self.acqs, self.guides) - } - mag_errs = Column( - [mag_errs.get(id, 0.0) for id in self["id"]], name="mag_err" - ) - self.add_column(mag_errs, index=self.colnames.index("mag") + 1) - - # Don't want maxmag column - if "maxmag" in self.colnames: - del self["maxmag"] - - # Customizations for ACAReviewTable. Don't really need 2 decimals on these. - for name in ("yang", "zang", "row", "col"): - self._default_formats[name] = ".1f" - - # Make mag column have an extra space for catalogs with all mags < 10.0 - self._default_formats["mag"] = "5.2f" - - if self.colnames[0] != "idx": - # Move 'idx' to first column. This is really painful currently. - self.add_column(Column(self["idx"], name="idx_temp"), index=0) - del self["idx"] - self.rename_column("idx_temp", "idx") - - @property - def t_ccds_bonus(self): - """Effective T_ccd for each guide star, including dynamic background bonus.""" - if not hasattr(self, "_t_ccds_bonus"): - self._t_ccds_bonus = get_t_ccds_bonus( - self.guides["mag"], - self.guides.t_ccd, - self.dyn_bgd_n_faint, - self.dyn_bgd_dt_ccd, - ) - return self._t_ccds_bonus - - @property - def guide_count(self): - if not hasattr(self, "_guide_count"): - mags = self.guides["mag"] - self._guide_count = guide_count(mags, self.t_ccds_bonus) - return self._guide_count - - @property - def guide_count_9th(self): - if not hasattr(self, "_guide_count_9th"): - mags = self.guides["mag"] - self._guide_count_9th = guide_count(mags, self.t_ccds_bonus, count_9th=True) - return self._guide_count_9th - - @property - def acq_count(self): - if not hasattr(self, "_acq_count"): - self._acq_count = np.sum(self.acqs["p_acq"]) - return self._acq_count - def run_aca_review( self, *, @@ -826,12 +687,6 @@ def review_status(self): return status - @property - def att_targ(self): - if not hasattr(self, "_att_targ"): - self._att_targ = self._calc_targ_from_aca(self.att, *self.target_offset) - return self._att_targ - @property def report_id(self): return round(self.att_targ.roll, 2) if self.is_roll_option else self.obsid @@ -846,18 +701,6 @@ def thumbs_down(self): n_crit = len(self.messages == "critical") return n_crit > 0 - @property - def is_OR(self): - """Return ``True`` if obsid corresponds to an OR.""" - if not hasattr(self, "_is_OR"): - self._is_OR = self.obsid < 38000 - return self._is_OR - - @property - def is_ER(self): - """Return ``True`` if obsid corresponds to an ER.""" - return not self.is_OR - def make_report(self): """Make report for acq and guide.""" if self.loud: @@ -1130,183 +973,6 @@ def get_formatted_messages(self): out = "\n".join(lines) + "\n\n" if lines else "" return out - def add_row_col(self): - """Add row and col columns if not present""" - if "row" in self.colnames: - return - - row, col = yagzag_to_pixels(self["yang"], self["zang"], allow_bad=True) - index = self.colnames.index("zang") + 1 - self.add_column(Column(row, name="row"), index=index) - self.add_column(Column(col, name="col"), index=index + 1) - - def check_catalog(self): - """Perform all star catalog checks.""" - for entry in self: - entry_type = entry["type"] - is_guide = entry_type in ("BOT", "GUI") - is_acq = entry_type in ("BOT", "ACQ") - is_fid = entry_type == "FID" - - if is_guide or is_fid: - self.check_guide_fid_position_on_ccd(entry) - - if is_guide: - star = self.guides.get_id(entry["id"]) - self.check_pos_err_guide(star) - self.check_imposters_guide(star) - self.check_too_bright_guide(star) - self.check_guide_is_candidate(star) - - if is_guide or is_acq: - self.check_bad_stars(entry) - - if is_fid: - fid = self.fids.get_id(entry["id"]) - self.check_fid_spoiler_score(entry["idx"], fid) - - self.check_guide_overlap() - self.check_guide_geometry() - self.check_acq_p2() - self.check_guide_count() - self.check_dither() - self.check_fid_count() - self.check_include_exclude() - - def check_guide_overlap(self): - """Check for overlapping tracked items. - - Overlap is defined as within 12 pixels. - """ - ok = np.in1d(self["type"], ("GUI", "BOT", "FID", "MON")) - idxs = np.flatnonzero(ok) - for idx1, idx2 in combinations(idxs, 2): - entry1 = self[idx1] - entry2 = self[idx2] - drow = entry1["row"] - entry2["row"] - dcol = entry1["col"] - entry2["col"] - if np.abs(drow) <= 12 and np.abs(dcol) <= 12: - msg = ( - "Overlapping track index (within 12 pix) " - f'idx [{entry1["idx"]}] and idx [{entry2["idx"]}]' - ) - self.add_message("critical", msg) - - def check_guide_geometry(self): - """Check for guide stars too tightly clustered. - - (1) Check for any set of n_guide-2 stars within 500" of each other. - The nominal check here is a cluster of 3 stars within 500". For - ERs this check is very unlikely to fail. For catalogs with only - 4 guide stars this will flag for any 2 nearby stars. - - This check will likely need some refinement. - - (2) Check for all stars being within 2500" of each other. - - """ - ok = np.in1d(self["type"], ("GUI", "BOT")) - guide_idxs = np.flatnonzero(ok) - n_guide = len(guide_idxs) - - if n_guide < 2: - msg = "Cannot check geometry with fewer than 2 guide stars" - self.add_message("critical", msg) - return - - def dist2(g1, g2): - out = (g1["yang"] - g2["yang"]) ** 2 + (g1["zang"] - g2["zang"]) ** 2 - return out - - # First check for any set of n_guide-2 stars within 500" of each other. - min_dist = 500 - min_dist2 = min_dist**2 - for idxs in combinations(guide_idxs, n_guide - 2): - for idx0, idx1 in combinations(idxs, 2): - # If any distance in this combination exceeds min_dist then - # the combination is OK. - if dist2(self[idx0], self[idx1]) > min_dist2: - break - else: - # Every distance was too small, issue a warning. - cat_idxs = [idx + 1 for idx in idxs] - msg = f'Guide indexes {cat_idxs} clustered within {min_dist}" radius' - - if self.man_angle_next > CREEP_AWAY_THRESHOLD: - msg += f" (man_angle_next > {CREEP_AWAY_THRESHOLD})" - self.add_message("critical", msg) - else: - msg += f" (man_angle_next <= {CREEP_AWAY_THRESHOLD})" - self.add_message("warning", msg) - - # Check for all stars within 2500" of each other - min_dist = 2500 - min_dist2 = min_dist**2 - for idx0, idx1 in combinations(guide_idxs, 2): - if dist2(self[idx0], self[idx1]) > min_dist2: - break - else: - msg = f'Guide stars all clustered within {min_dist}" radius' - self.add_message("warning", msg) - - def check_guide_fid_position_on_ccd(self, entry): - """Check position of guide stars and fid lights on CCD.""" - # Shortcuts and translate y/z to yaw/pitch - dither_guide_y = self.dither_guide.y - dither_guide_p = self.dither_guide.z - - # Set "dither" for FID to be pseudodither of 5.0 to give 1 pix margin - # Set "track phase" dither for BOT GUI to max guide dither over - # interval or 20.0 if undefined. TO DO: hand the guide guide dither - dither_track_y = 5.0 if (entry["type"] == "FID") else dither_guide_y - dither_track_p = 5.0 if (entry["type"] == "FID") else dither_guide_p - - row_lim = ACA.max_ccd_row - ACA.CCD["window_pad"] - col_lim = ACA.max_ccd_col - ACA.CCD["window_pad"] - - def sign(axis): - """Return sign of the corresponding entry value. - - Note that np.sign returns 0 if the value is 0.0, not the right thing here. - """ - return -1 if (entry[axis] < 0) else 1 - - track_lims = { - "row": (row_lim - dither_track_y * ACA.ARC_2_PIX) * sign("row"), - "col": (col_lim - dither_track_p * ACA.ARC_2_PIX) * sign("col"), - } - - for axis in ("row", "col"): - track_delta = abs(track_lims[axis]) - abs(entry[axis]) - track_delta = np.round( - track_delta, decimals=1 - ) # Official check is to 1 decimal - for delta_lim, category in ((3.0, "critical"), (5.0, "info")): - if track_delta < delta_lim: - text = ( - f"Less than {delta_lim} pix edge margin {axis} " - f"lim {track_lims[axis]:.1f} " - f"val {entry[axis]:.1f} " - f"delta {track_delta:.1f}" - ) - self.add_message(category, text, idx=entry["idx"]) - break - - # TO DO: acq star position check: - # For acq stars, the distance to the row/col padded limits are also confirmed, - # but code to track which boundary is exceeded (row or column) is not present. - # Note from above that the pix_row_pad used for row_lim has 7 more pixels of padding - # than the pix_col_pad used to determine col_lim. - # acq_edge_delta = min((row_lim - dither_acq_y / ang_per_pix) - abs(pixel_row), - # (col_lim - dither_acq_p / ang_per_pix) - abs(pixel_col)) - # if ((entry_type =~ /BOT|ACQ/) and (acq_edge_delta < (-1 * 12))){ - # push @orange_warn, sprintf "alarm [%2d] Acq Off (padded) CCD by > 60 arcsec.\n",i - # } - # elsif ((entry_type =~ /BOT|ACQ/) and (acq_edge_delta < 0)){ - # push @{self->{fyi}}, - # sprintf "alarm [%2d] Acq Off (padded) CCD (P_ACQ should be < .5)\n",i - # } - def add_message(self, category, text, **kwargs): r"""Add message to internal messages list. @@ -1319,288 +985,6 @@ def add_message(self, category, text, **kwargs): message.update(kwargs) self.messages.append(message) - def check_acq_p2(self): - """Check acquisition catalog safing probability.""" - P2 = -np.log10(self.acqs.calc_p_safe()) - P2 = np.round(P2, decimals=2) # Official check is to 2 decimals - obs_type = "OR" if self.is_OR else "ER" - P2_lim = 2.0 if self.is_OR else 3.0 - if P2 < P2_lim: - self.add_message( - "critical", f"P2: {P2:.2f} less than {P2_lim} for {obs_type}" - ) - elif P2 < P2_lim + 1: - self.add_message( - "warning", f"P2: {P2:.2f} less than {P2_lim + 1} for {obs_type}" - ) - - def check_include_exclude(self): - """Check for included or excluded guide or acq stars or fids (info)""" - call_args = self.call_args - for typ in ("acq", "guide", "fid"): - for action in ("include", "exclude"): - ids = call_args.get(f"{action}_ids_{typ}") - if ids is not None: - msg = f"{action}d {typ} ID(s): {ids}" - - # Check for halfwidths. This really only applies to - # include_halfws_acq, but having it here in the loop doesn't hurt. - halfws = call_args.get(f"{action}_halfws_{typ}") - if halfws is not None: - msg = msg + f" halfwidths(s): {halfws}" - - self.add_message("info", msg) - - def check_guide_count(self): - """ - Check for sufficient guide star fractional count. - - Also check for multiple very-bright stars - - """ - obs_type = "ER" if self.is_ER else "OR" - count_9th_lim = 3.0 - if self.is_ER and np.round(self.guide_count_9th, decimals=2) < count_9th_lim: - # Determine the threshold 9th mag equivalent value at the effective guide t_ccd - mag9 = snr_mag_for_t_ccd(self.guides.t_ccd, 9.0, -10.9) - self.add_message( - "critical", - ( - f"{obs_type} count of 9th ({mag9:.1f} for {self.guides.t_ccd:.1f}C)" - f" mag guide stars {self.guide_count_9th:.2f} < {count_9th_lim}" - ), - ) - - # Rounded guide count - guide_count_round = np.round(self.guide_count, decimals=2) - - # Set critical guide_count threshold - # For observations with creep-away in place as a mitigation for end of observation - # roll error, we can accept a lower guide_count (3.5 instead of 4.0). - # See https://occweb.cfa.harvard.edu/twiki/bin/view/Aspect/StarWorkingGroupMeeting2023x03x15 - if self.is_OR: - count_lim = 3.5 if (self.man_angle_next <= CREEP_AWAY_THRESHOLD) else 4.0 - else: - count_lim = 6.0 - - if guide_count_round < count_lim: - self.add_message( - "critical", - f"{obs_type} count of guide stars {self.guide_count:.2f} < {count_lim}", - ) - # If in the 3.5 to 4.0 range, this probably deserves a warning. - elif count_lim == 3.5 and guide_count_round < 4.0: - self.add_message( - "warning", - f"{obs_type} count of guide stars {self.guide_count:.2f} < 4.0", - ) - - bright_cnt_lim = 1 if self.is_OR else 3 - if np.count_nonzero(self.guides["mag"] < 5.5) > bright_cnt_lim: - self.add_message( - "caution", - f"{obs_type} with more than {bright_cnt_lim} stars brighter than 5.5.", - ) - - # Requested slots for guide stars and mon windows - n_guide_or_mon_request = self.call_args["n_guide"] - - # Actual guide stars - n_guide = len(self.guides) - - # Actual mon windows. For catalogs from pickles from proseco < 5.0 - # self.mons might be initialized to a NoneType or not be an attribute so - # handle that as 0 monitor windows. - try: - n_mon = len(self.mons) - except (TypeError, AttributeError): - n_mon = 0 - - # Different number of guide stars than requested - if n_guide + n_mon != n_guide_or_mon_request: - if n_mon == 0: - # Usual case - msg = ( - f"{obs_type} with {n_guide} guides " - f"but {n_guide_or_mon_request} were requested" - ) - else: - msg = ( - f"{obs_type} with {n_guide} guides and {n_mon} monitor(s) " - f"but {n_guide_or_mon_request} guides or mon slots were requested" - ) - self.add_message("caution", msg) - - # Caution for any "unusual" guide star request - typical_n_guide = 5 if self.is_OR else 8 - if n_guide_or_mon_request != typical_n_guide: - or_mon_slots = " or mon slots" if n_mon > 0 else "" - msg = ( - f"{obs_type} with" - f" {n_guide_or_mon_request} guides{or_mon_slots} requested but" - f" {typical_n_guide} is typical" - ) - self.add_message("caution", msg) - - # Add a check that for ORs with guide count between 3.5 and 4.0, the - # dither is 4 arcsec if dynamic background not enabled. - def check_dither(self): - """Check dither. - - This presently checks that dither is 4x4 arcsec if dynamic background is not in - use and the field has a low guide_count. - """ - - # Skip check if guide_count is 4.0 or greater - if self.guide_count >= 4.0: - return - - # Skip check if dynamic backround is enabled (inferred from dyn_bgd_n_faint) - if self.dyn_bgd_n_faint > 0: - return - - # Check that dither is <= 4x4 arcsec - if self.dither_guide.y > 4.0 or self.dither_guide.z > 4.0: - self.add_message( - "critical", - f"guide_count {self.guide_count:.2f} and dither > 4x4 arcsec", - ) - - def check_pos_err_guide(self, star): - """Warn on stars with larger POS_ERR (warning at 1" critical at 2")""" - agasc_id = star["id"] - idx = self.get_id(agasc_id)["idx"] - # POS_ERR is in milliarcsecs in the table - pos_err = star["POS_ERR"] * 0.001 - for limit, category in ((2.0, "critical"), (1.25, "warning")): - if np.round(pos_err, decimals=2) > limit: - self.add_message( - category, - ( - f"Guide star {agasc_id} POS_ERR {pos_err:.2f}, limit" - f" {limit} arcsec" - ), - idx=idx, - ) - break - - def check_imposters_guide(self, star): - """Warn on stars with larger imposter centroid offsets""" - - # Borrow the imposter offset method from starcheck - def imposter_offset(cand_mag, imposter_mag): - """Get imposter offset. - - For a given candidate star and the pseudomagnitude of the brightest 2x2 - imposter calculate the max offset of the imposter counts are at the edge of - the 6x6 (as if they were in one pixel). This is somewhat the inverse of - proseco.get_pixmag_for_offset. - """ - cand_counts = mag_to_count_rate(cand_mag) - spoil_counts = mag_to_count_rate(imposter_mag) - return spoil_counts * 3 * 5 / (spoil_counts + cand_counts) - - agasc_id = star["id"] - idx = self.get_id(agasc_id)["idx"] - offset = imposter_offset(star["mag"], star["imp_mag"]) - for limit, category in ((4.0, "critical"), (2.5, "warning")): - if np.round(offset, decimals=1) > limit: - self.add_message( - category, - f"Guide star imposter offset {offset:.1f}, limit {limit} arcsec", - idx=idx, - ) - break - - def check_guide_is_candidate(self, star): - """Critical for guide star that is not a valid guide candidate. - - This can occur for a manually included guide star. In rare cases - the star may still be acceptable and ACA review can accept the warning. - """ - if not self.guides.get_candidates_mask(star): - agasc_id = star["id"] - idx = self.get_id(agasc_id)["idx"] - self.add_message( - "critical", - f"Guide star {agasc_id} does not meet guide candidate criteria", - idx=idx, - ) - - def check_too_bright_guide(self, star): - """Warn on guide stars that may be too bright. - - - Critical if within 2 * mag_err of the hard 5.2 limit, caution within 3 * mag_err - - """ - agasc_id = star["id"] - idx = self.get_id(agasc_id)["idx"] - mag_err = star["mag_err"] - for mult, category in ((2, "critical"), (3, "caution")): - if star["mag"] - (mult * mag_err) < 5.2: - self.add_message( - category, - ( - f"Guide star {agasc_id} within {mult}*mag_err of 5.2 " - f"(mag_err={mag_err:.2f})" - ), - idx=idx, - ) - break - - def check_bad_stars(self, entry): - """Check if entry (guide or acq) is in bad star set from proseco - - :param entry: ACAReviewTable row - :return: None - """ - if entry["id"] in ACA.bad_star_set: - msg = f'Star {entry["id"]} is in proseco bad star set' - self.add_message("critical", msg, idx=entry["idx"]) - - def check_fid_spoiler_score(self, idx, fid): - """ - Check the spoiler warnings for fid - - :param idx: catalog index of fid entry being checked - :param fid: corresponding row of ``fids`` table - :return: None - """ - if fid["spoiler_score"] == 0: - return - - fid_id = fid["id"] - category_map = {"yellow": "warning", "red": "critical"} - - for spoiler in fid["spoilers"]: - msg = ( - f'Fid {fid_id} has {spoiler["warn"]} spoiler: star {spoiler["id"]} with' - f' mag {spoiler["mag"]:.2f}' - ) - self.add_message(category_map[spoiler["warn"]], msg, idx=idx) - - def check_fid_count(self): - """ - Check for the correct number of fids. - - :return: None - """ - obs_type = "ER" if self.is_ER else "OR" - - if len(self.fids) != self.n_fid: - msg = ( - f"{obs_type} has {len(self.fids)} fids but {self.n_fid} were requested" - ) - self.add_message("critical", msg) - - # Check for "typical" number of fids for an OR / ER (3 or 0) - typical_n_fid = 3 if self.is_OR else 0 - if self.n_fid != typical_n_fid: - msg = ( - f"{obs_type} requested {self.n_fid} fids but {typical_n_fid} is typical" - ) - self.add_message("caution", msg) - @classmethod def from_ocat(cls, obsid, t_ccd=-5, man_angle=5, date=None, roll=None, **kwargs): """Return an AcaReviewTable object using OCAT to specify key information. @@ -1629,3 +1013,67 @@ def from_ocat(cls, obsid, t_ccd=-5, man_angle=5, date=None, roll=None, **kwargs) aca = get_aca_catalog(**params_proseco) acar = cls(aca) return acar + + +check_acq_p2 = checks.acar_check_wrapper(checks.check_acq_p2) +check_bad_stars = checks.acar_check_wrapper(checks.check_bad_stars) +check_dither = checks.acar_check_wrapper(checks.check_dither) +check_fid_count = checks.acar_check_wrapper(checks.check_fid_count) +check_fid_spoiler_score = checks.acar_check_wrapper(checks.check_fid_spoiler_score) +check_guide_count = checks.acar_check_wrapper(checks.check_guide_count) +check_guide_fid_position_on_ccd = checks.acar_check_wrapper( + checks.check_guide_fid_position_on_ccd +) +check_guide_geometry = checks.acar_check_wrapper(checks.check_guide_geometry) +check_guide_is_candidate = checks.acar_check_wrapper(checks.check_guide_is_candidate) +check_guide_overlap = checks.acar_check_wrapper(checks.check_guide_overlap) +check_imposters_guide = checks.acar_check_wrapper(checks.check_imposters_guide) +check_include_exclude = checks.acar_check_wrapper(checks.check_include_exclude) +check_pos_err_guide = checks.acar_check_wrapper(checks.check_pos_err_guide) +check_too_bright_guide = checks.acar_check_wrapper(checks.check_too_bright_guide) + + +def check_catalog(acar: ACAChecksTable) -> None: + """Perform all star catalog checks.""" + msgs: list[Message] = [] + for entry in acar: + entry_type = entry["type"] + is_guide = entry_type in ("BOT", "GUI") + is_acq = entry_type in ("BOT", "ACQ") + is_fid = entry_type == "FID" + + if is_guide or is_fid: + msgs += checks.check_guide_fid_position_on_ccd(acar, entry) + + if is_guide: + star = acar.guides.get_id(entry["id"]) + msgs += checks.check_pos_err_guide(acar, star) + msgs += checks.check_imposters_guide(acar, star) + msgs += checks.check_too_bright_guide(acar, star) + msgs += checks.check_guide_is_candidate(acar, star) + + if is_guide or is_acq: + msgs += checks.check_bad_stars(entry) + + if is_fid: + fid = acar.fids.get_id(entry["id"]) + msgs += checks.check_fid_spoiler_score(entry["idx"], fid) + + msgs += checks.check_guide_overlap(acar) + msgs += checks.check_guide_geometry(acar) + msgs += checks.check_acq_p2(acar) + msgs += checks.check_guide_count(acar) + msgs += checks.check_dither(acar) + msgs += checks.check_fid_count(acar) + msgs += checks.check_include_exclude(acar) + + messages = [ + { + key: val + for key in ("category", "text", "idx") + if (val := getattr(msg, key)) is not None + } + for msg in msgs + ] + + acar.messages.extend(messages) diff --git a/sparkles/messages.py b/sparkles/messages.py new file mode 100644 index 0000000..c55bbd3 --- /dev/null +++ b/sparkles/messages.py @@ -0,0 +1,36 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class Message: + """Message class.""" + + category: str + text: str + idx: int | None = None + + def __getitem__(self, key): + return getattr(self, key) + + +class MessagesList(list[Message]): + categories = ("all", "info", "caution", "warning", "critical", "none") + + def __eq__(self, other): + if isinstance(other, str): + return [msg for msg in self if msg["category"] == other] + else: + return super().__eq__(other) + + def __ge__(self, other): + if isinstance(other, str): + other_idx = self.categories.index(other) + return [ + msg + for msg in self + if self.categories.index(msg["category"]) >= other_idx + ] + else: + return super().__ge__(other) diff --git a/sparkles/roll_optimize.py b/sparkles/roll_optimize.py index 5e6b836..edc3902 100644 --- a/sparkles/roll_optimize.py +++ b/sparkles/roll_optimize.py @@ -339,6 +339,7 @@ def get_roll_options( :return: None """ + from sparkles.core import check_catalog if self.loud: print(f" Exploring roll options {method=}") @@ -359,7 +360,7 @@ def get_roll_options( # Special case, first roll option is self but with obsid set to roll acar = deepcopy(self) - acar.check_catalog() + check_catalog(acar) acar.is_roll_option = True roll_options = [ { @@ -414,7 +415,7 @@ def get_roll_options( acar = self.__class__(aca_rolled, obsid=self.obsid, is_roll_option=True) # Do the review and set up messages attribute - acar.check_catalog() + check_catalog(acar) roll_option = { "acar": acar, diff --git a/sparkles/tests/test_checks.py b/sparkles/tests/test_checks.py index a2bc50c..698a115 100644 --- a/sparkles/tests/test_checks.py +++ b/sparkles/tests/test_checks.py @@ -15,6 +15,18 @@ from Quaternion import Quat from sparkles import ACAReviewTable, get_t_ccds_bonus +from sparkles.aca_checks_table import ACAChecksTable +from sparkles.core import ( + check_acq_p2, + check_catalog, + check_dither, + check_guide_count, + check_guide_geometry, + check_imposters_guide, + check_include_exclude, + check_pos_err_guide, + check_too_bright_guide, +) def test_check_slice_index(): @@ -30,15 +42,16 @@ def test_check_slice_index(): assert np.all(acar1[name] == acar[name][item]) -def test_check_P2(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_check_P2(aca_review_table): """Test the check of acq P2""" stars = StarsTable.empty() stars.add_fake_constellation(n_stars=3, mag=10.25) aca = get_aca_catalog(**STD_INFO, stars=stars, dark=DARK40) - acar = ACAReviewTable(aca) + acar = aca_review_table(aca) # Check P2 for an OR (default obsid=0) - acar.check_acq_p2() + check_acq_p2(acar) assert len(acar.messages) == 1 msg = acar.messages[0] assert msg["category"] == "critical" @@ -53,15 +66,16 @@ def test_check_P2(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_acq_p2() + acar = aca_review_table(aca) + check_acq_p2(acar) assert len(acar.messages) == 1 msg = acar.messages[0] assert msg["category"] == "critical" assert "less than 3.0 for ER" in msg["text"] -def test_n_guide_check_not_enough_stars(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_n_guide_check_not_enough_stars(aca_review_table): """Test the check that number of guide stars selected is as requested""" stars = StarsTable.empty() @@ -72,14 +86,15 @@ def test_n_guide_check_not_enough_stars(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_guide_count() + acar = aca_review_table(aca) + check_guide_count(acar) assert acar.messages == [ {"text": "OR with 4 guides but 5 were requested", "category": "caution"} ] -def test_guide_is_candidate(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_guide_is_candidate(aca_review_table): """Test the check that guide star meets candidate star requirements Make a star catalog with a CLASS=3 star and force include it for guide. @@ -94,8 +109,8 @@ def test_guide_is_candidate(): include_ids_guide=[100], raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_catalog() + acar = aca_review_table(aca) + check_catalog(acar) assert acar.messages == [ { "text": "Guide star 100 does not meet guide candidate criteria", @@ -106,7 +121,8 @@ def test_guide_is_candidate(): ] -def test_n_guide_check_atypical_request(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_n_guide_check_atypical_request(aca_review_table): """Test the check that number of guide stars selected is typical""" stars = StarsTable.empty() @@ -117,14 +133,15 @@ def test_n_guide_check_atypical_request(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_guide_count() + acar = aca_review_table(aca) + check_guide_count(acar) assert acar.messages == [ {"text": "OR with 4 guides requested but 5 is typical", "category": "caution"} ] -def test_n_guide_mon_check_atypical_request(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_n_guide_mon_check_atypical_request(aca_review_table): """Test the check that number of guide stars selected is typical in the case where are monitors""" @@ -138,8 +155,8 @@ def test_n_guide_mon_check_atypical_request(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_guide_count() + acar = aca_review_table(aca) + check_guide_count(acar) assert acar.messages == [ { "text": "OR with 6 guides or mon slots requested but 5 is typical", @@ -156,7 +173,8 @@ def test_n_guide_mon_check_atypical_request(): @pytest.mark.parametrize("vals", vals) -def test_guide_count_dyn_bgd_bonus(vals): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_guide_count_dyn_bgd_bonus(vals, aca_review_table): n_guide, leg_guide_count, dyn_guide_count = vals stars = StarsTable.empty() @@ -173,14 +191,15 @@ def test_guide_count_dyn_bgd_bonus(vals): assert len(aca_dyn.guides) == n_guide assert np.all(aca_leg.guides["mag"] == aca_dyn.guides["mag"]) - acar_leg = ACAReviewTable(aca_leg) - acar_dyn = ACAReviewTable(aca_dyn) + acar_leg = aca_review_table(aca_leg) + acar_dyn = aca_review_table(aca_dyn) # Computed guide counts without / with dyn_bgd_n_faint=2 assert np.isclose(acar_leg.guide_count, leg_guide_count, rtol=0, atol=0.1) assert np.isclose(acar_dyn.guide_count, dyn_guide_count, rtol=0, atol=0.1) -def test_n_guide_too_few_guide_or_mon(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_n_guide_too_few_guide_or_mon(aca_review_table): """Test the check that the number of actual guide and mon stars is what was requested""" @@ -194,8 +213,8 @@ def test_n_guide_too_few_guide_or_mon(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_guide_count() + acar = aca_review_table(aca) + check_guide_count(acar) assert acar.messages == [ { "category": "caution", @@ -211,7 +230,8 @@ def test_n_guide_too_few_guide_or_mon(): ] -def test_guide_count_er1(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_guide_count_er1(aca_review_table): """Test the check that an ER has enough fractional guide stars by guide_count""" # This configuration should have not enough bright stars @@ -223,15 +243,16 @@ def test_guide_count_er1(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_guide_count() + acar = aca_review_table(aca) + check_guide_count(acar) assert len(acar.messages) == 1 msg = acar.messages[0] assert msg["category"] == "critical" assert "ER count of 9th" in msg["text"] -def test_guide_count_er2(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_guide_count_er2(aca_review_table): # This configuration should have not enough stars overall stars = StarsTable.empty() stars.add_fake_constellation(n_stars=3, mag=[8.5, 8.5, 8.5]) @@ -241,17 +262,18 @@ def test_guide_count_er2(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_guide_count() + acar = aca_review_table(aca) + check_guide_count(acar) assert acar.messages == [ {"text": "ER count of guide stars 3.00 < 6.0", "category": "critical"}, {"text": "ER with 3 guides but 8 were requested", "category": "caution"}, ] -def test_guide_count_er3(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_guide_count_er3(aca_review_table): # And this configuration should have about the bare minumum (of course better - # to do this with programmatic instead of fixed checks... TODO) + # to do this with programmatic instead of fixed .. TODO) stars = StarsTable.empty() stars.add_fake_constellation(n_stars=6, mag=[8.5, 8.5, 8.5, 9.9, 9.9, 9.9]) aca = get_aca_catalog( @@ -260,14 +282,15 @@ def test_guide_count_er3(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_guide_count() + acar = aca_review_table(aca) + check_guide_count(acar) assert acar.messages == [ {"text": "ER with 6 guides but 8 were requested", "category": "caution"} ] -def test_guide_count_er4(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_guide_count_er4(aca_review_table): # This configuration should not warn with too many really bright stars # (allowed to have 3 stars brighter than 6.1) stars = StarsTable.empty() @@ -281,14 +304,15 @@ def test_guide_count_er4(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_guide_count() + acar = aca_review_table(aca) + check_guide_count(acar) assert acar.messages == [ {"text": "ER with 6 guides but 8 were requested", "category": "caution"} ] -def test_include_exclude(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_include_exclude(aca_review_table): """Test INFO statement for explicitly included/excluded entries""" stars = StarsTable.empty() stars.add_fake_constellation(n_stars=8, mag=np.linspace(7.0, 8.75, 8)) @@ -303,8 +327,8 @@ def test_include_exclude(): include_halfws_acq=[140, 120], raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_include_exclude() + acar = aca_review_table(aca) + check_include_exclude(acar) assert acar.messages == [ { "category": "info", @@ -316,7 +340,8 @@ def test_include_exclude(): ] -def test_guide_count_er5(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_guide_count_er5(aca_review_table): # This configuration should warn with too many bright stars # (has > 3.0 stars brighter than 5.5 stars = StarsTable.empty() @@ -329,15 +354,16 @@ def test_guide_count_er5(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_guide_count() + acar = aca_review_table(aca) + check_guide_count(acar) assert acar.messages == [ {"text": "ER with more than 3 stars brighter than 5.5.", "category": "caution"}, {"text": "ER with 6 guides but 8 were requested", "category": "caution"}, ] -def test_guide_count_or(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_guide_count_or(aca_review_table): """Test the check that an OR has enough fractional guide stars by guide_count""" stars = StarsTable.empty() stars.add_fake_constellation(n_stars=5, mag=[7.0, 7.0, 10.3, 10.3, 10.3]) @@ -347,15 +373,16 @@ def test_guide_count_or(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_guide_count() + acar = aca_review_table(aca) + check_guide_count(acar) assert acar.messages == [ {"text": "OR count of guide stars 2.00 < 4.0", "category": "critical"}, {"text": "OR with 2 guides but 5 were requested", "category": "caution"}, ] -def test_ok_number_bright_guide_stars(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_ok_number_bright_guide_stars(aca_review_table): # This configuration should not warn with too many really bright stars # (allowed to have 1 stars brighter than 5.5) stars = StarsTable.empty() @@ -367,14 +394,15 @@ def test_ok_number_bright_guide_stars(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_guide_count() + acar = aca_review_table(aca) + check_guide_count(acar) assert acar.messages == [ {"text": "OR with 4 guides but 5 were requested", "category": "caution"} ] -def test_too_many_bright_stars(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_too_many_bright_stars(aca_review_table): # This configuration should warn with too many bright stars # (has > 1.0 stars brighter than 5.5 stars = StarsTable.empty() @@ -387,15 +415,16 @@ def test_too_many_bright_stars(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_guide_count() + acar = aca_review_table(aca) + check_guide_count(acar) assert len(acar.messages) == 1 msg = acar.messages[0] assert msg["category"] == "caution" assert "OR with more than 1 stars brighter than 5.5." in msg["text"] -def test_low_guide_count(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_low_guide_count(aca_review_table): """Test that a 3.5 to 4.0 guide_count observation gets a critical warning on guide_count if man_angle_next > 5 (no creep-away).""" # Set a scenario with guide_count in the 3.5 to 4.0 range and confirm a @@ -408,18 +437,19 @@ def test_low_guide_count(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) + acar = aca_review_table(aca) # Confirm the guide_count is in the range we want for the test to be valid assert acar.guide_count <= 4.0 and acar.guide_count > 3.5 assert acar.man_angle_next > 5 - acar.check_guide_count() + check_guide_count(acar) assert acar.messages == [ {"text": "OR count of guide stars 3.65 < 4.0", "category": "critical"}, {"text": "OR with 4 guides but 5 were requested", "category": "caution"}, ] -def test_low_guide_count_creep_away(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_low_guide_count_creep_away(aca_review_table): """Test that a 3.5 to 4.0 guide_count observation does not get a critical warning on guide_count if man_angle_next <= 5 (creep-away).""" # Set a scenario with guide_count in the 3.5 to 4.0 range but with @@ -433,17 +463,18 @@ def test_low_guide_count_creep_away(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) + acar = aca_review_table(aca) # Confirm the guide_count is in the range we want for the test to be valid assert acar.guide_count <= 4.0 and acar.guide_count > 3.5 - acar.check_guide_count() + check_guide_count(acar) assert acar.messages == [ {"text": "OR count of guide stars 3.65 < 4.0", "category": "warning"}, {"text": "OR with 4 guides but 5 were requested", "category": "caution"}, ] -def test_reduced_dither_low_guide_count(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_reduced_dither_low_guide_count(aca_review_table): """Test that a 3.5 to 4.0 guide_count observation without dynamic background in use (dyn_bgd_n_faint == 0) does not get a dither critical warning for 4x4 arcsec dither. """ @@ -457,17 +488,18 @@ def test_reduced_dither_low_guide_count(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) + acar = aca_review_table(aca) # Confirm the guide_count is in the range we want for the test to be valid assert acar.guide_count <= 4.0 and acar.guide_count > 3.5 # Run the dither check - acar.check_dither() + check_dither(acar) assert len(acar.messages) == 0 -def test_get_t_ccds_bonus_1(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_get_t_ccds_bonus_1(aca_review_table): mags = [1, 10, 2, 11, 3, 4] t_ccd = 10 @@ -488,7 +520,8 @@ def test_get_t_ccds_bonus_1(): assert np.all(t_ccds == [10, 10, 10, 10, 10, 10]) -def test_get_t_ccds_bonus_min_anchor(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_get_t_ccds_bonus_min_anchor(aca_review_table): mags = [1, 10, 2] t_ccd = 10 t_ccds = get_t_ccds_bonus(mags, t_ccd, dyn_bgd_n_faint=2, dyn_bgd_dt_ccd=-1) @@ -505,7 +538,8 @@ def test_get_t_ccds_bonus_small_catalog(): assert np.all(t_ccds == [10]) -def test_not_reduced_dither_low_guide_count(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_not_reduced_dither_low_guide_count(aca_review_table): """Test that a 3.5 to 4.0 guide_count observation without dynamic background in use (dyn_bgd_n_faint == 0) gets a dither critical warning for 8x8 arcsec dither. """ @@ -519,19 +553,20 @@ def test_not_reduced_dither_low_guide_count(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) + acar = aca_review_table(aca) # Confirm the guide_count is in the range we want for the test to be valid assert acar.guide_count <= 4.0 and acar.guide_count > 3.5 # Run the dither check - acar.check_dither() + check_dither(acar) assert acar.messages == [ {"text": "guide_count 3.65 and dither > 4x4 arcsec", "category": "critical"} ] -def test_not_reduced_dither_low_guide_count_dyn_bgd(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_not_reduced_dither_low_guide_count_dyn_bgd(aca_review_table): """Test that a 3.5 to 4.0 guide_count observation with dynamic background in use (dyn_bgd_n_faint > 0) does not get a dither critical warning for 8x8 arcsec dither. """ @@ -547,14 +582,15 @@ def test_not_reduced_dither_low_guide_count_dyn_bgd(): dark=DARK40, raise_exc=True, ) - acar = ACAReviewTable(aca) + acar = aca_review_table(aca) # Confirm the guide_count is in the range we want for the test to be valid assert acar.guide_count <= 4.0 and acar.guide_count > 3.5 - acar.check_dither() + check_dither(acar) assert len(acar.messages) == 0 -def test_pos_err_on_guide(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_pos_err_on_guide(aca_review_table): """Test the check that no guide star has large POS_ERR""" stars = StarsTable.empty() stars.add_fake_star(id=100, yang=100, zang=-200, POS_ERR=2010, mag=8.0) @@ -576,14 +612,14 @@ def test_pos_err_on_guide(): include_ids_guide=[100, 101], ) # Must force 100, 101, pos_err too big - acar = ACAReviewTable(aca) + acar = aca_review_table(aca) # 103 not selected because pos_err > 1.25 arcsec assert acar.guides["id"].tolist() == [100, 101, 102] # Run pos err checks for guide in aca.guides: - acar.check_pos_err_guide(guide) + check_pos_err_guide(acar, guide) assert len(acar.messages) == 2 msg = acar.messages[0] @@ -619,7 +655,8 @@ def test_guide_overlap(): ) -def test_guide_edge_check(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_guide_edge_check(aca_review_table): stars = StarsTable.empty() dither = 8 row_lim = CCD["row_max"] - CCD["row_pad"] - CCD["window_pad"] - dither / 5 @@ -645,8 +682,8 @@ def test_guide_edge_check(): raise_exc=True, include_ids_guide=np.arange(1, 7), ) - acar = ACAReviewTable(aca) - acar.check_catalog() + acar = aca_review_table(aca) + check_catalog(acar) assert acar.messages == [ { @@ -674,7 +711,8 @@ def test_guide_edge_check(): @pytest.mark.parametrize("exp_warn", [False, True]) -def test_imposters_on_guide(exp_warn): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_imposters_on_guide(exp_warn, aca_review_table): """Test the check for imposters by adding one imposter to a fake star""" stars = StarsTable.empty() # Add two stars because separate P2 tests seem to break with just one star @@ -695,8 +733,8 @@ def test_imposters_on_guide(exp_warn): dark=dark_with_badpix, raise_exc=True, ) - acar = ACAReviewTable(aca) - acar.check_imposters_guide(aca.guides.get_id(110)) + acar = aca_review_table(aca) + check_imposters_guide(acar, aca.guides.get_id(110)) if exp_warn: assert len(acar.messages) == 1 msg = acar.messages[0] @@ -706,7 +744,8 @@ def test_imposters_on_guide(exp_warn): assert len(acar.messages) == 0 -def test_bad_star_set(proseco_agasc_1p7): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_bad_star_set(proseco_agasc_1p7, aca_review_table): # This faint star is no longer in proseco_agasc >= 1.8 so we use 1.7 bad_id = 1248994952 star = agasc.get_star(bad_id) @@ -717,8 +756,8 @@ def test_bad_star_set(proseco_agasc_1p7): dark=DARK40, include_ids_guide=[bad_id], ) - acar = ACAReviewTable(aca) - acar.check_catalog() + acar = aca_review_table(aca) + check_catalog(acar) assert acar.messages == [ { "text": "Guide star 1248994952 does not meet guide candidate criteria", @@ -735,7 +774,8 @@ def test_bad_star_set(proseco_agasc_1p7): ] -def test_too_bright_guide_magerr(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_too_bright_guide_magerr(aca_review_table): """Test the check for too-bright guide stars within mult*mag_err of 5.2""" stars = StarsTable.empty() # Add two stars because separate P2 tests seem to break with just one star @@ -746,14 +786,15 @@ def test_too_bright_guide_magerr(): aca = get_aca_catalog( **mod_std_info(n_fid=0), stars=stars, dark=DARK40, raise_exc=True ) - acar = ACAReviewTable(aca) - acar.check_too_bright_guide(aca.guides.get_id(100)) + acar = aca_review_table(aca) + check_too_bright_guide(acar, aca.guides.get_id(100)) msg = acar.messages[0] assert msg["category"] == "critical" assert "2*mag_err of 5.2" in msg["text"] -def test_check_fid_spoiler_score(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_check_fid_spoiler_score(aca_review_table): """Test checking fid spoiler score""" stars = StarsTable.empty() # def add_fake_stars_from_fid(self, fid_id=1, offset_y=0, offset_z=0, mag=7.0, @@ -768,8 +809,8 @@ def test_check_fid_spoiler_score(): assert np.all(aca.fids.cand_fids["spoiler_score"] == [4, 4, 4, 4, 1, 0]) - acar = ACAReviewTable(aca) - acar.check_catalog() + acar = aca_review_table(aca) + check_catalog(acar) assert acar.messages == [ { "text": "Fid 1 has red spoiler: star 108 with mag 9.00", @@ -784,7 +825,8 @@ def test_check_fid_spoiler_score(): ] -def test_check_fid_count1(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_check_fid_count1(aca_review_table): """Test checking fid count""" stars = StarsTable.empty() stars.add_fake_constellation(n_stars=8) @@ -792,29 +834,31 @@ def test_check_fid_count1(): aca = get_aca_catalog( stars=stars, **mod_std_info(detector="HRC-S", sim_offset=40000) ) - acar = ACAReviewTable(aca) - acar.check_catalog() + acar = aca_review_table(aca) + check_catalog(acar) assert acar.messages == [ {"text": "OR has 2 fids but 3 were requested", "category": "critical"} ] -def test_check_fid_count2(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_check_fid_count2(aca_review_table): """Test checking fid count""" stars = StarsTable.empty() stars.add_fake_constellation(n_stars=8) aca = get_aca_catalog(stars=stars, **mod_std_info(detector="HRC-S", n_fid=2)) - acar = ACAReviewTable(aca) - acar.check_catalog() + acar = aca_review_table(aca) + check_catalog(acar) assert acar.messages == [ {"text": "OR requested 2 fids but 3 is typical", "category": "caution"} ] -def test_check_guide_geometry(): +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +def test_check_guide_geometry(aca_review_table): """Test the checks of geometry (not all within 2500" not N-2 within 500")""" yangs = np.array([1, 0, -1, 0]) zangs = np.array([0, 1, 0, -1]) @@ -829,7 +873,7 @@ def test_check_guide_geometry(): aca = get_aca_catalog(**STD_INFO, stars=stars, dark=DARK40) acar = aca.get_review_table() - acar.check_guide_geometry() + check_guide_geometry(acar) if fail: assert len(acar.messages) == 1 msg = acar.messages[0] @@ -847,7 +891,7 @@ def test_check_guide_geometry(): stars.add_fake_star(yang=y * size, zang=z * size, mag=7.0) aca = get_aca_catalog(**STD_INFO, stars=stars, dark=DARK40) acar = aca.get_review_table() - acar.check_guide_geometry() + check_guide_geometry(acar) assert len(acar.messages) == 1 msg = acar.messages[0] @@ -866,7 +910,7 @@ def test_check_guide_geometry(): aca = get_aca_catalog(**STD_INFO, man_angle_next=5.0, stars=stars, dark=DARK40) acar = aca.get_review_table() - acar.check_guide_geometry() + check_guide_geometry(acar) assert len(acar.messages) == 1 msg = acar.messages[0] From 705eea986e36314adad6090a2b92ee9df39c4bbc Mon Sep 17 00:00:00 2001 From: Tom Aldcroft Date: Tue, 2 Jan 2024 15:57:55 -0500 Subject: [PATCH 2/3] Rename type aliases --- sparkles/checks.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/sparkles/checks.py b/sparkles/checks.py index a176b98..822fed9 100644 --- a/sparkles/checks.py +++ b/sparkles/checks.py @@ -5,7 +5,7 @@ import numpy as np import proseco.characteristics as ACA from chandra_aca.transform import mag_to_count_rate, snr_mag_for_t_ccd -from proseco.core import CatalogRow, StarTableRow +from proseco.core import ACACatalogTableRow, StarsTableRow from sparkles.aca_checks_table import ACAChecksTable from sparkles.messages import Message @@ -123,7 +123,7 @@ def dist2(g1, g2): def check_guide_fid_position_on_ccd( - acar: ACAChecksTable, entry: CatalogRow + acar: ACAChecksTable, entry: ACACatalogTableRow ) -> list[Message]: """Check position of guide stars and fid lights on CCD.""" msgs = [] @@ -354,7 +354,7 @@ def check_dither(acar: ACAChecksTable) -> list[Message]: return msgs -def check_pos_err_guide(acar: ACAChecksTable, star: StarTableRow) -> list[Message]: +def check_pos_err_guide(acar: ACAChecksTable, star: StarsTableRow) -> list[Message]: """Warn on stars with larger POS_ERR (warning at 1" critical at 2")""" msgs = [] agasc_id = star["id"] @@ -377,7 +377,7 @@ def check_pos_err_guide(acar: ACAChecksTable, star: StarTableRow) -> list[Messag return msgs -def check_imposters_guide(acar: ACAChecksTable, star: StarTableRow) -> list[Message]: +def check_imposters_guide(acar: ACAChecksTable, star: StarsTableRow) -> list[Message]: """Warn on stars with larger imposter centroid offsets""" # Borrow the imposter offset method from starcheck @@ -410,7 +410,9 @@ def imposter_offset(cand_mag, imposter_mag): return msgs -def check_guide_is_candidate(acar: ACAChecksTable, star: StarTableRow) -> list[Message]: +def check_guide_is_candidate( + acar: ACAChecksTable, star: StarsTableRow +) -> list[Message]: """Critical for guide star that is not a valid guide candidate. This can occur for a manually included guide star. In rare cases @@ -430,7 +432,7 @@ def check_guide_is_candidate(acar: ACAChecksTable, star: StarTableRow) -> list[M return msgs -def check_too_bright_guide(acar: ACAChecksTable, star: StarTableRow) -> list[Message]: +def check_too_bright_guide(acar: ACAChecksTable, star: StarsTableRow) -> list[Message]: """Warn on guide stars that may be too bright. - Critical if within 2 * mag_err of the hard 5.2 limit, caution within 3 * mag_err @@ -456,7 +458,7 @@ def check_too_bright_guide(acar: ACAChecksTable, star: StarTableRow) -> list[Mes return msgs -def check_bad_stars(entry: CatalogRow) -> list[Message]: +def check_bad_stars(entry: ACACatalogTableRow) -> list[Message]: """Check if entry (guide or acq) is in bad star set from proseco :param entry: ACATable row From ee2f79e6e50c4f97c25163dbf9f132c591594db8 Mon Sep 17 00:00:00 2001 From: Tom Aldcroft Date: Tue, 2 Jan 2024 16:07:09 -0500 Subject: [PATCH 3/3] More renaming --- sparkles/__init__.py | 2 +- ...aca_checks_table.py => aca_check_table.py} | 2 +- sparkles/checks.py | 30 ++++----- sparkles/core.py | 6 +- sparkles/tests/test_checks.py | 66 +++++++++---------- 5 files changed, 52 insertions(+), 54 deletions(-) rename sparkles/{aca_checks_table.py => aca_check_table.py} (99%) diff --git a/sparkles/__init__.py b/sparkles/__init__.py index 2c988cf..dc8f57e 100644 --- a/sparkles/__init__.py +++ b/sparkles/__init__.py @@ -2,7 +2,7 @@ __version__ = ska_helpers.get_version(__package__) -from .aca_checks_table import get_t_ccds_bonus # noqa: F401 +from .aca_check_table import get_t_ccds_bonus # noqa: F401 from .core import ACAReviewTable, run_aca_review # noqa: F401 diff --git a/sparkles/aca_checks_table.py b/sparkles/aca_check_table.py similarity index 99% rename from sparkles/aca_checks_table.py rename to sparkles/aca_check_table.py index 6230af9..ddd67e7 100644 --- a/sparkles/aca_checks_table.py +++ b/sparkles/aca_check_table.py @@ -46,7 +46,7 @@ def get_t_ccds_bonus(mags, t_ccd, dyn_bgd_n_faint, dyn_bgd_dt_ccd): return t_ccds -class ACAChecksTable(ACATable): +class ACACheckTable(ACATable): messages = MetaAttribute() def __init__(self, *args, **kwargs): diff --git a/sparkles/checks.py b/sparkles/checks.py index 822fed9..c0a5b63 100644 --- a/sparkles/checks.py +++ b/sparkles/checks.py @@ -7,7 +7,7 @@ from chandra_aca.transform import mag_to_count_rate, snr_mag_for_t_ccd from proseco.core import ACACatalogTableRow, StarsTableRow -from sparkles.aca_checks_table import ACAChecksTable +from sparkles.aca_check_table import ACACheckTable from sparkles.messages import Message # Observations with man_angle_next less than or equal to CREEP_AWAY_THRESHOLD @@ -24,7 +24,7 @@ def acar_check_wrapper(func): """ @functools.wraps(func) - def wrapper(acar: ACAChecksTable, *args, **kwargs): + def wrapper(acar: ACACheckTable, *args, **kwargs): msgs: list[Message] = func(acar, *args, **kwargs) messages = [ { @@ -40,7 +40,7 @@ def wrapper(acar: ACAChecksTable, *args, **kwargs): return wrapper -def check_guide_overlap(acar: ACAChecksTable) -> list[Message]: +def check_guide_overlap(acar: ACACheckTable) -> list[Message]: """Check for overlapping tracked items. Overlap is defined as within 12 pixels. @@ -62,7 +62,7 @@ def check_guide_overlap(acar: ACAChecksTable) -> list[Message]: return msgs -def check_guide_geometry(acar: ACAChecksTable) -> list[Message]: +def check_guide_geometry(acar: ACACheckTable) -> list[Message]: """Check for guide stars too tightly clustered. (1) Check for any set of n_guide-2 stars within 500" of each other. @@ -123,7 +123,7 @@ def dist2(g1, g2): def check_guide_fid_position_on_ccd( - acar: ACAChecksTable, entry: ACACatalogTableRow + acar: ACACheckTable, entry: ACACatalogTableRow ) -> list[Message]: """Check position of guide stars and fid lights on CCD.""" msgs = [] @@ -186,7 +186,7 @@ def sign(axis): # } -def check_acq_p2(acar: ACAChecksTable) -> list[Message]: +def check_acq_p2(acar: ACACheckTable) -> list[Message]: """Check acquisition catalog safing probability.""" msgs = [] P2 = -np.log10(acar.acqs.calc_p_safe()) @@ -202,7 +202,7 @@ def check_acq_p2(acar: ACAChecksTable) -> list[Message]: return msgs -def check_include_exclude(acar: ACAChecksTable) -> list[Message]: +def check_include_exclude(acar: ACACheckTable) -> list[Message]: """Check for included or excluded guide or acq stars or fids (info)""" msgs = [] call_args = acar.call_args @@ -222,7 +222,7 @@ def check_include_exclude(acar: ACAChecksTable) -> list[Message]: return msgs -def check_guide_count(acar: ACAChecksTable) -> list[Message]: +def check_guide_count(acar: ACACheckTable) -> list[Message]: """ Check for sufficient guide star fractional count. @@ -327,7 +327,7 @@ def check_guide_count(acar: ACAChecksTable) -> list[Message]: # Add a check that for ORs with guide count between 3.5 and 4.0, the # dither is 4 arcsec if dynamic background not enabled. -def check_dither(acar: ACAChecksTable) -> list[Message]: +def check_dither(acar: ACACheckTable) -> list[Message]: """Check dither. This presently checks that dither is 4x4 arcsec if dynamic background is not in @@ -354,7 +354,7 @@ def check_dither(acar: ACAChecksTable) -> list[Message]: return msgs -def check_pos_err_guide(acar: ACAChecksTable, star: StarsTableRow) -> list[Message]: +def check_pos_err_guide(acar: ACACheckTable, star: StarsTableRow) -> list[Message]: """Warn on stars with larger POS_ERR (warning at 1" critical at 2")""" msgs = [] agasc_id = star["id"] @@ -377,7 +377,7 @@ def check_pos_err_guide(acar: ACAChecksTable, star: StarsTableRow) -> list[Messa return msgs -def check_imposters_guide(acar: ACAChecksTable, star: StarsTableRow) -> list[Message]: +def check_imposters_guide(acar: ACACheckTable, star: StarsTableRow) -> list[Message]: """Warn on stars with larger imposter centroid offsets""" # Borrow the imposter offset method from starcheck @@ -410,9 +410,7 @@ def imposter_offset(cand_mag, imposter_mag): return msgs -def check_guide_is_candidate( - acar: ACAChecksTable, star: StarsTableRow -) -> list[Message]: +def check_guide_is_candidate(acar: ACACheckTable, star: StarsTableRow) -> list[Message]: """Critical for guide star that is not a valid guide candidate. This can occur for a manually included guide star. In rare cases @@ -432,7 +430,7 @@ def check_guide_is_candidate( return msgs -def check_too_bright_guide(acar: ACAChecksTable, star: StarsTableRow) -> list[Message]: +def check_too_bright_guide(acar: ACACheckTable, star: StarsTableRow) -> list[Message]: """Warn on guide stars that may be too bright. - Critical if within 2 * mag_err of the hard 5.2 limit, caution within 3 * mag_err @@ -495,7 +493,7 @@ def check_fid_spoiler_score(idx, fid) -> list[Message]: return msgs -def check_fid_count(acar: ACAChecksTable) -> list[Message]: +def check_fid_count(acar: ACACheckTable) -> list[Message]: """ Check for the correct number of fids. diff --git a/sparkles/core.py b/sparkles/core.py index 6ab89e3..abfcf89 100644 --- a/sparkles/core.py +++ b/sparkles/core.py @@ -19,7 +19,7 @@ from proseco.core import MetaAttribute from sparkles import checks -from sparkles.aca_checks_table import ACAChecksTable +from sparkles.aca_check_table import ACACheckTable from sparkles.messages import Message, MessagesList from sparkles.roll_optimize import RollOptimizeMixin @@ -568,7 +568,7 @@ def get_summary_text(acas): return "\n".join(lines) -class ACAReviewTable(ACAChecksTable, RollOptimizeMixin): +class ACAReviewTable(ACACheckTable, RollOptimizeMixin): # Whether this instance is a roll option (controls how HTML report page is formatted) is_roll_option = MetaAttribute() roll_options = MetaAttribute() @@ -1033,7 +1033,7 @@ def from_ocat(cls, obsid, t_ccd=-5, man_angle=5, date=None, roll=None, **kwargs) check_too_bright_guide = checks.acar_check_wrapper(checks.check_too_bright_guide) -def check_catalog(acar: ACAChecksTable) -> None: +def check_catalog(acar: ACACheckTable) -> None: """Perform all star catalog checks.""" msgs: list[Message] = [] for entry in acar: diff --git a/sparkles/tests/test_checks.py b/sparkles/tests/test_checks.py index 698a115..8b67254 100644 --- a/sparkles/tests/test_checks.py +++ b/sparkles/tests/test_checks.py @@ -15,7 +15,7 @@ from Quaternion import Quat from sparkles import ACAReviewTable, get_t_ccds_bonus -from sparkles.aca_checks_table import ACAChecksTable +from sparkles.aca_check_table import ACACheckTable from sparkles.core import ( check_acq_p2, check_catalog, @@ -42,7 +42,7 @@ def test_check_slice_index(): assert np.all(acar1[name] == acar[name][item]) -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_check_P2(aca_review_table): """Test the check of acq P2""" stars = StarsTable.empty() @@ -74,7 +74,7 @@ def test_check_P2(aca_review_table): assert "less than 3.0 for ER" in msg["text"] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_n_guide_check_not_enough_stars(aca_review_table): """Test the check that number of guide stars selected is as requested""" @@ -93,7 +93,7 @@ def test_n_guide_check_not_enough_stars(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_guide_is_candidate(aca_review_table): """Test the check that guide star meets candidate star requirements @@ -121,7 +121,7 @@ def test_guide_is_candidate(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_n_guide_check_atypical_request(aca_review_table): """Test the check that number of guide stars selected is typical""" @@ -140,7 +140,7 @@ def test_n_guide_check_atypical_request(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_n_guide_mon_check_atypical_request(aca_review_table): """Test the check that number of guide stars selected is typical in the case where are monitors""" @@ -173,7 +173,7 @@ def test_n_guide_mon_check_atypical_request(aca_review_table): @pytest.mark.parametrize("vals", vals) -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_guide_count_dyn_bgd_bonus(vals, aca_review_table): n_guide, leg_guide_count, dyn_guide_count = vals stars = StarsTable.empty() @@ -198,7 +198,7 @@ def test_guide_count_dyn_bgd_bonus(vals, aca_review_table): assert np.isclose(acar_dyn.guide_count, dyn_guide_count, rtol=0, atol=0.1) -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_n_guide_too_few_guide_or_mon(aca_review_table): """Test the check that the number of actual guide and mon stars is what was requested""" @@ -230,7 +230,7 @@ def test_n_guide_too_few_guide_or_mon(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_guide_count_er1(aca_review_table): """Test the check that an ER has enough fractional guide stars by guide_count""" @@ -251,7 +251,7 @@ def test_guide_count_er1(aca_review_table): assert "ER count of 9th" in msg["text"] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_guide_count_er2(aca_review_table): # This configuration should have not enough stars overall stars = StarsTable.empty() @@ -270,7 +270,7 @@ def test_guide_count_er2(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_guide_count_er3(aca_review_table): # And this configuration should have about the bare minumum (of course better # to do this with programmatic instead of fixed .. TODO) @@ -289,7 +289,7 @@ def test_guide_count_er3(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_guide_count_er4(aca_review_table): # This configuration should not warn with too many really bright stars # (allowed to have 3 stars brighter than 6.1) @@ -311,7 +311,7 @@ def test_guide_count_er4(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_include_exclude(aca_review_table): """Test INFO statement for explicitly included/excluded entries""" stars = StarsTable.empty() @@ -340,7 +340,7 @@ def test_include_exclude(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_guide_count_er5(aca_review_table): # This configuration should warn with too many bright stars # (has > 3.0 stars brighter than 5.5 @@ -362,7 +362,7 @@ def test_guide_count_er5(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_guide_count_or(aca_review_table): """Test the check that an OR has enough fractional guide stars by guide_count""" stars = StarsTable.empty() @@ -381,7 +381,7 @@ def test_guide_count_or(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_ok_number_bright_guide_stars(aca_review_table): # This configuration should not warn with too many really bright stars # (allowed to have 1 stars brighter than 5.5) @@ -401,7 +401,7 @@ def test_ok_number_bright_guide_stars(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_too_many_bright_stars(aca_review_table): # This configuration should warn with too many bright stars # (has > 1.0 stars brighter than 5.5 @@ -423,7 +423,7 @@ def test_too_many_bright_stars(aca_review_table): assert "OR with more than 1 stars brighter than 5.5." in msg["text"] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_low_guide_count(aca_review_table): """Test that a 3.5 to 4.0 guide_count observation gets a critical warning on guide_count if man_angle_next > 5 (no creep-away).""" @@ -448,7 +448,7 @@ def test_low_guide_count(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_low_guide_count_creep_away(aca_review_table): """Test that a 3.5 to 4.0 guide_count observation does not get a critical warning on guide_count if man_angle_next <= 5 (creep-away).""" @@ -473,7 +473,7 @@ def test_low_guide_count_creep_away(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_reduced_dither_low_guide_count(aca_review_table): """Test that a 3.5 to 4.0 guide_count observation without dynamic background in use (dyn_bgd_n_faint == 0) does not get a dither critical warning for 4x4 arcsec dither. @@ -498,7 +498,7 @@ def test_reduced_dither_low_guide_count(aca_review_table): assert len(acar.messages) == 0 -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_get_t_ccds_bonus_1(aca_review_table): mags = [1, 10, 2, 11, 3, 4] t_ccd = 10 @@ -520,7 +520,7 @@ def test_get_t_ccds_bonus_1(aca_review_table): assert np.all(t_ccds == [10, 10, 10, 10, 10, 10]) -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_get_t_ccds_bonus_min_anchor(aca_review_table): mags = [1, 10, 2] t_ccd = 10 @@ -538,7 +538,7 @@ def test_get_t_ccds_bonus_small_catalog(): assert np.all(t_ccds == [10]) -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_not_reduced_dither_low_guide_count(aca_review_table): """Test that a 3.5 to 4.0 guide_count observation without dynamic background in use (dyn_bgd_n_faint == 0) gets a dither critical warning for 8x8 arcsec dither. @@ -565,7 +565,7 @@ def test_not_reduced_dither_low_guide_count(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_not_reduced_dither_low_guide_count_dyn_bgd(aca_review_table): """Test that a 3.5 to 4.0 guide_count observation with dynamic background in use (dyn_bgd_n_faint > 0) does not get a dither critical warning for 8x8 arcsec dither. @@ -589,7 +589,7 @@ def test_not_reduced_dither_low_guide_count_dyn_bgd(aca_review_table): assert len(acar.messages) == 0 -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_pos_err_on_guide(aca_review_table): """Test the check that no guide star has large POS_ERR""" stars = StarsTable.empty() @@ -655,7 +655,7 @@ def test_guide_overlap(): ) -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_guide_edge_check(aca_review_table): stars = StarsTable.empty() dither = 8 @@ -711,7 +711,7 @@ def test_guide_edge_check(aca_review_table): @pytest.mark.parametrize("exp_warn", [False, True]) -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_imposters_on_guide(exp_warn, aca_review_table): """Test the check for imposters by adding one imposter to a fake star""" stars = StarsTable.empty() @@ -744,7 +744,7 @@ def test_imposters_on_guide(exp_warn, aca_review_table): assert len(acar.messages) == 0 -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_bad_star_set(proseco_agasc_1p7, aca_review_table): # This faint star is no longer in proseco_agasc >= 1.8 so we use 1.7 bad_id = 1248994952 @@ -774,7 +774,7 @@ def test_bad_star_set(proseco_agasc_1p7, aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_too_bright_guide_magerr(aca_review_table): """Test the check for too-bright guide stars within mult*mag_err of 5.2""" stars = StarsTable.empty() @@ -793,7 +793,7 @@ def test_too_bright_guide_magerr(aca_review_table): assert "2*mag_err of 5.2" in msg["text"] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_check_fid_spoiler_score(aca_review_table): """Test checking fid spoiler score""" stars = StarsTable.empty() @@ -825,7 +825,7 @@ def test_check_fid_spoiler_score(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_check_fid_count1(aca_review_table): """Test checking fid count""" stars = StarsTable.empty() @@ -842,7 +842,7 @@ def test_check_fid_count1(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_check_fid_count2(aca_review_table): """Test checking fid count""" stars = StarsTable.empty() @@ -857,7 +857,7 @@ def test_check_fid_count2(aca_review_table): ] -@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACAChecksTable)) +@pytest.mark.parametrize("aca_review_table", (ACAReviewTable, ACACheckTable)) def test_check_guide_geometry(aca_review_table): """Test the checks of geometry (not all within 2500" not N-2 within 500")""" yangs = np.array([1, 0, -1, 0])