diff --git a/acis_thermal_check/__init__.py b/acis_thermal_check/__init__.py index aa0aa1d..98d4c0a 100644 --- a/acis_thermal_check/__init__.py +++ b/acis_thermal_check/__init__.py @@ -2,9 +2,8 @@ __version__ = ska_helpers.get_version(__package__) -from acis_thermal_check.acis_obs import acis_filter, fetch_ocat_data from acis_thermal_check.main import ACISThermalCheck, DPABoardTempCheck -from acis_thermal_check.utils import get_acis_limits, get_options, mylog +from acis_thermal_check.utils import get_options, mylog def test(*args, **kwargs): diff --git a/acis_thermal_check/acis_obs.py b/acis_thermal_check/acis_obs.py deleted file mode 100644 index 0cc4feb..0000000 --- a/acis_thermal_check/acis_obs.py +++ /dev/null @@ -1,371 +0,0 @@ -import numpy as np -from cxotime import CxoTime -from kadi.commands.states import decode_power - -from acis_thermal_check.utils import mylog - - -def who_in_fp(simpos=80655): - """ - Returns a string telling you which instrument is in - the Focal Plane. "launchlock" is returned because that's a - position we never expect to see the sim in - it's an indicator - to the user that there's a problem. - - Also, The ranges for detector sections use the max and min hard - stop locations, and they also split the difference between "I" - and "S" for each instrument. - - input: - TSC position (simpos) - INTEGER - - output - String indicating what is in the focal plane - "launchlock" - default - "ACIS-I" - "ACIS-S" - "HRC-I" - "HRC-S" - """ - is_in_the_fp = "launchlock" - - # Set the value of is_in_the_fp to the appropriate value. It will default - # to "launchlock" if no value matches - if 104839 >= simpos >= 82109: - is_in_the_fp = "ACIS-I" - elif 82108 >= simpos >= 70736: - is_in_the_fp = "ACIS-S" - elif -20000 >= simpos >= -86147: - is_in_the_fp = "HRC-I" - elif -86148 >= simpos >= -104362: - is_in_the_fp = "HRC-S" - - # return the string indicating which instrument is in the Focal Plane - return is_in_the_fp - - -def fetch_ocat_data(obsid_list): - """ - Take a list of obsids and return the following data from - the obscat for each: grating status, CCD count, if S3 is on, - and the number of expected counts - - Parameters - ---------- - obsid_list : list of ints - The obsids to get the obscat data from. - - Returns - ------- - A dict of NumPy arrays of the above properties. - """ - import requests - from astropy.io import ascii - from ska_helpers.retry import RetryError, retry_call - - warn = ( - "Could not get the table from the Obscat to " - "determine which observations can go to -109 C. " - "Any violations of eligible observations should " - "be hand-checked." - ) - # Only bother with this check if obsids are found - if len(obsid_list) > 0: - # The following uses a request call to the obscat which explicitly - # asks for text formatting so that the output can be ingested into - # an AstroPy table. - urlbase = "https://cda.harvard.edu/srservices/ocatDetails.do?format=text" - obsid_list = ",".join([str(obsid) for obsid in obsid_list]) - params = {"obsid": obsid_list} - # First fetch the information from the obsid itself - got_table = True - try: - resp = retry_call( - requests.get, - [urlbase], - {"params": params}, - tries=4, - delay=1, - ) - except (requests.ConnectionError, RetryError): - got_table = False - else: - if not resp.ok: - got_table = False - else: - warn = "No obsids to check, may be a vehicle load--please check if not." - got_table = False - if got_table: - tab = ascii.read(resp.text, header_start=0, data_start=2) - tab.sort("OBSID") - # Now we have to find all of the obsids in each sequence and then - # compute the complete exposure for each sequence - seq_nums = np.unique( - [str(sn) for sn in tab["SEQ_NUM"].data.astype("str") if sn != " "], - ) - seq_num_list = ",".join(seq_nums) - obsids = tab["OBSID"].data.astype("int") - cnt_rate = tab["EST_CNT_RATE"].data.astype("float64") - params = {"seqNum": seq_num_list} - got_seq_table = True - try: - resp = retry_call( - requests.get, - [urlbase], - {"params": params}, - tries=4, - delay=1, - ) - except (requests.ConnectionError, RetryError): - got_seq_table = False - else: - if not resp.ok: - got_seq_table = False - if not got_seq_table: - # We weren't able to get a valid sequence table for some - # reason, so we cannot check for -109 data, but we proceed - # with the rest of the review regardless - mylog.warning(warn) - return None - tab_seq = ascii.read(resp.text, header_start=0, data_start=2) - app_exp = np.zeros_like(cnt_rate) - seq_nums = tab_seq["SEQ_NUM"].data.astype("str") - for i, row in enumerate(tab): - sn = str(row["SEQ_NUM"]) - if sn == " ": - continue - j = np.where(sn == seq_nums)[0] - app_exp[i] += np.float64(tab_seq["APP_EXP"][j]).sum() - app_exp *= 1000.0 - table_dict = { - "obsid": np.array(obsids), - "grating": tab["GRAT"].data, - "cnt_rate": cnt_rate, - "app_exp": app_exp, - "spectra_max_count": tab["SPECTRA_MAX_COUNT"].data.astype("int"), - "obs_cycle": tab["OBS_CYCLE"].data, - } - else: - # We weren't able to get a valid table for some reason, so - # we cannot check for hot observations, but we proceed with the - # rest of the review regardless - mylog.warning(warn) - table_dict = None - return table_dict - - -def find_obsid_intervals(cmd_states, load_start): - """ - User reads the SKA commanded states archive, via - a call to the SKA kadi.commands.states.get_states, - between the user specified START and STOP times. - - Problem is, ALL commanded states that were stored - in the archive will be returned. So then you call: - - find_obsid_intervals(cmd_states) - - And this will find the obsid intervals. - What this program does is to extract the time interval for - each OBSID. Said interval start is defined by a - startScience comment, and the interval end is - defined by the first stopScience command that follows. - - When the interval has been found, - a dict element is created from the value of - states data at the time point of the first NPNT - line seen - *minus* the trans_keys, tstart and tstop - times. The values of datestart and datestop are - the XTZ00000 and AA000000 times. This dict - is appended to a Master list of all obsid intervals - and this list is returned. - - Notes: The obsid filtering method includes the - configuration from the last OBSID, through - a setup for the present OBSID, through the - XTZ - AA000, down to the power down. - - - This might show a cooling from the - last config, temp changes due to some - possible maneuvering, past shutdown - """ - # - # Some inits - # - - # a little initialization - firstpow = False - xtztime = None - - # EXTRACTING THE OBSERVATIONS - # - # Find the first line with a WSPOW00000 in it. This is the start of - # the interval. Then get the first XTZ line, the NPNT line, the - # AA000000 line, and lastly the next WSPOW00000 line. - # This constitutes one observation. - - obsid_interval_list = [] - - for eachstate in cmd_states: - # Make sure we skip maneuver obsids explicitly - if 60000 > eachstate["obsid"] >= 38001: - continue - - # Only check states which are at least partially - # within the load being reviewed - if eachstate["tstop"] < load_start: - continue - - pow_cmd = eachstate["power_cmd"] - - # is this the first WSPOW of the interval? - if pow_cmd in ["WSPOW00000", "WSVIDALLDN"] and not firstpow: - firstpow = True - datestart = eachstate["datestart"] - tstart = eachstate["tstart"] - - # Process the power command which turns things on - if pow_cmd.startswith("WSPOW") and pow_cmd != "WSPOW00000" and firstpow: - ccds = decode_power(pow_cmd)["ccds"].replace(" ", ",")[:-1] - - # Process the first XTZ0000005 line you see - if pow_cmd in ["XTZ0000005", "XCZ0000005"] and (xtztime is None and firstpow): - xtztime = eachstate["tstart"] - # MUST fix the instrument now - instrument = who_in_fp(eachstate["simpos"]) - ccd_count = eachstate["ccd_count"] - - # Process the first AA00000000 line you see - if pow_cmd == "AA00000000" and firstpow: - datestop = eachstate["datestop"] - tstop = eachstate["tstop"] - - # now calculate the exposure time - if xtztime is not None: - # Having found the startScience and stopScience, you have an - # OBSID interval. Now form the element and append it to - # the Master List. We add the text version of who is in - # the focal plane - - obsid_dict = { - "datestart": datestart, - "datestop": datestop, - "tstart": tstart, - "tstop": tstop, - "ccds": ccds, - "start_science": xtztime, - "obsid": eachstate["obsid"], - "instrument": instrument, - "ccd_count": ccd_count, - } - obsid_interval_list.append(obsid_dict) - - # now clear out the data values - firstpow = False - xtztime = None - - # End of LOOP for eachstate in cmd_states: - - # sort based on obsid - obsid_interval_list.sort(key=lambda x: x["obsid"]) - # Now we add the stuff we get from ocat_data - obsids = [e["obsid"] for e in obsid_interval_list] - ocat_data = fetch_ocat_data(obsids) - # For a vehicle load, or if the connection to the ocat server - # fails (rare), we will get no data from the ocat so we only - # add this info if we need to. In the case of a failed connection - # to the OCAT server, -109 C checks should be done by hand - if ocat_data is not None: - ocat_keys = list(ocat_data.keys()) - ocat_keys.remove("obsid") - for i, obsid in enumerate(obsids): - # The obscat doesn't have info for cold ECS observations - if obsid > 60000: - continue - for key in ocat_keys: - obsid_interval_list[i][key] = ocat_data[key][i] - - # re-sort based on tstart - obsid_interval_list.sort(key=lambda x: x["tstart"]) - return obsid_interval_list - - -def hrc_science_obs_filter(obsid_interval_list): - """ - This method will filter *OUT* any HRC science observations from the - input obsid interval list. Filtered are obs that have either - HRC-I" or HRC-S" as the science instrument, AND an obsid LESS THAN - 50,000 - """ - acis_and_ecs_only = [] - for eachobservation in obsid_interval_list: - if ( - eachobservation["instrument"].startswith("ACIS-") - or eachobservation["obsid"] >= 60000 - ): - acis_and_ecs_only.append(eachobservation) - return acis_and_ecs_only - - -def acis_filter(obsid_interval_list): - """ - This method will filter between the different types of - ACIS observations: ACIS-I, ACIS-S, "hot" ACIS, and - cold science-orbit ECS. - """ - acis_hot = [] - acis_s = [] - acis_i = [] - cold_ecs = [] - - # This is the approximate date after which we start applying new - # rules for going to hotter temperatures - new_hot_start = CxoTime("2022:318:00:00:00").secs - - mylog.debug("OBSID\tCNT_RATE\tAPP_EXP\tNUM_CTS\tGRATING\tCCDS\tSPEC_MAX_CNT\tCYCLE") - for eachobs in obsid_interval_list: - # First we check that we got ocat data using "grating" - hot_acis = False - if "grating" in eachobs: - eachobs["num_counts"] = int(eachobs["cnt_rate"] * eachobs["app_exp"]) - # First check to see if this is an S3 observation - mylog.debug( - f"{eachobs['obsid']}\t{eachobs['cnt_rate']}\t" - f"{eachobs['app_exp']*1.0e-3}\t{eachobs['num_counts']}\t" - f"{eachobs['grating']}\t{eachobs['ccds']}\t" - f"{eachobs['spectra_max_count']}\t{eachobs['obs_cycle']}", - ) - low_ct = False - # "New" hot ACIS category: - # 1. Cycle 23 and later - # 2. spectra_max_count must be greater than 0 - # 3. Start time must be after ~NOV1422 load - if ( - eachobs["obs_cycle"] >= 23 - and eachobs["spectra_max_count"] > 0 - and eachobs["tstart"] > new_hot_start - ): - if eachobs["instrument"] == "ACIS-I": - low_ct = eachobs["spectra_max_count"] <= 1000 - elif eachobs["instrument"] == "ACIS-S": - low_ct = eachobs["spectra_max_count"] <= 2000 - else: - # otherwise, fall back to modified "old" criterion - # of less than 300 total expected counts - low_ct = eachobs["num_counts"] < 300 - # Also check grating status - hot_acis = (eachobs["grating"] == "HETG") or low_ct - eachobs["hot_acis"] = hot_acis - if hot_acis: - acis_hot.append(eachobs) - else: - if eachobs["instrument"] == "ACIS-S": - acis_s.append(eachobs) - elif eachobs["instrument"] == "ACIS-I": - acis_i.append(eachobs) - elif eachobs["instrument"] == "HRC-S" and eachobs["obsid"] >= 60000: - cold_ecs.append(eachobs) - else: - raise RuntimeError( - f"Cannot determine what kind of thermal " - f"limit {eachobs['obsid']} should have!", - ) - return acis_i, acis_s, acis_hot, cold_ecs diff --git a/acis_thermal_check/apps/acisfp_check.py b/acis_thermal_check/apps/acisfp_check.py index 36da62c..b4fcb30 100755 --- a/acis_thermal_check/apps/acisfp_check.py +++ b/acis_thermal_check/apps/acisfp_check.py @@ -14,21 +14,13 @@ import sys import matplotlib +import numpy as np from astropy.table import Table +from chandra_limits import ACISFPLimit from cxotime import CxoTime from ska_matplotlib import cxctime2plotdate from acis_thermal_check import ACISThermalCheck, get_options, mylog - -# -# Import ACIS-specific observation extraction, filtering -# and attribute support routines. -# -from acis_thermal_check.acis_obs import ( - acis_filter, - find_obsid_intervals, - hrc_science_obs_filter, -) from acis_thermal_check.utils import PredictPlot, paint_perigee # Matplotlib setup @@ -37,17 +29,11 @@ class ACISFPCheck(ACISThermalCheck): + _limit_class = ACISFPLimit + def __init__(self): - valid_limits = {"PITCH": [(1, 3.0), (99, 3.0)], "TSCPOS": [(1, 2.5), (99, 2.5)]} + valid_limits = [(1, 2.0), (50, 1.0), (99, 2.0)] hist_limit = [(-120.0, -100.0)] - limits_map = { - "planning.data_quality.high.acisi": "acis_i", - "planning.data_quality.high.aciss": "acis_s", - "planning.data_quality.high.aciss_hot": "acis_hot", - "planning.data_quality.high.cold_ecs": "cold_ecs", - "planning.warning.high": "planning_hi", - "safety.caution.high": "yellow_hi", - } super().__init__( "fptemp", "acisfp", @@ -55,12 +41,10 @@ def __init__(self): hist_limit, other_telem=["1dahtbon"], other_map={"1dahtbon": "dh_heater", "fptemp_11": "fptemp"}, - limits_map=limits_map, ) # Create an empty observation list which will hold the results. This # list contains all ACIS and all ECS observations. self.acis_and_ecs_obs = [] - self.acis_hot_obs = [] def _calc_model_supp(self, model, state_times, states, ephem, state0): """ @@ -114,7 +98,9 @@ def _calc_model_supp(self, model, state_times, states, ephem, state0): model.comp["1cbat"].set_data(-53.0) model.comp["sim_px"].set_data(-120.0) - def make_prediction_plots(self, outdir, states, temps, load_start): + def make_prediction_plots( + self, outdir, states, temps, load_start, upper_limit, lower_limit + ): """ Make plots of the thermal prediction as well as associated commanded states. @@ -154,7 +140,7 @@ def make_prediction_plots(self, outdir, states, temps, load_start): w1 = None # Make plots of FPTEMP and pitch vs time, looping over # three different temperature ranges - ylim = [(-120, -79), (-120, -119), (-120.0, -107.5)] + ylim = [(-120, -79), (-120, -119), (-120.0, -103.5)] ypos = [-110.0, -119.35, -116] capwidth = [2.0, 0.1, 0.4] textypos = [-108.0, -119.3, -115.7] @@ -178,18 +164,17 @@ def make_prediction_plots(self, outdir, states, temps, load_start): load_start=load_start, ) plots[name].ax.set_title(self.msid.upper(), loc="left", pad=10) - # Draw a horizontal line indicating cold ECS cutoff - plots[name].add_limit_line(self.limits["cold_ecs"], "Cold ECS", ls="--") - # Draw a horizontal line showing the ACIS-I cutoff - plots[name].add_limit_line(self.limits["acis_i"], "ACIS-I", ls="--") - # Draw a horizontal line showing the ACIS-S cutoff - plots[name].add_limit_line(self.limits["acis_s"], "ACIS-S", ls="--") - # Draw a horizontal line showing the hot ACIS-S cutoff - plots[name].add_limit_line(self.limits["acis_hot"], "Hot ACIS-S", ls="--") - # Draw a horizontal line showing the planning warning limit - plots[name].add_limit_line(self.limits["planning_hi"], "Planning", ls="-") - # Draw a horizontal line showing the safety caution limit - plots[name].add_limit_line(self.limits["yellow_hi"], "Yellow", ls="-") + # Draw the planning limit line on the plot (broken up + # according to condition) + upper_limit.plot( + fig_ax=(plots[name].fig, plots[name].ax), + lw=3, + zorder=2, + use_colors=True, + show_changes=False, + ) + # Draw the yellow limit line on the plot + plots[name].add_limit_line(self.limits["yellow_hi"], lw=3) # Get the width of this plot to make the widths of all the # prediction plots the same if i == 0: @@ -243,17 +228,6 @@ def make_prediction_plots(self, outdir, states, temps, load_start): capthick=2, label="ACIS-S", ) - plots[name].ax.errorbar( - [0.0, 0.0], - [1.0, 1.0], - xerr=1.0, - lw=2, - xlolims=True, - color="saddlebrown", - capsize=4, - capthick=2, - label="Hot ACIS-S", - ) # Make the legend on the temperature plot plots[name].ax.legend( @@ -309,189 +283,27 @@ def make_prediction_viols(self, temps, states, load_start): - science_viols """ - # extract the OBSID's from the commanded states. NOTE: this contains all - # observations including ECS runs and HRC observations - observation_intervals = find_obsid_intervals(states, load_start) - - # Filter out any HRC science observations BUT keep ACIS ECS observations - self.acis_and_ecs_obs = hrc_science_obs_filter(observation_intervals) - - times = self.predict_model.times - - mylog.info( - f"MAKE VIOLS Checking for limit violations in " - f"{len(self.acis_and_ecs_obs)} total science observations", + # Extract the prediction violations and the limit objects + viols, upper_limit, lower_limit = super().make_prediction_viols( + temps, states, load_start ) - viols = {} - - # ------------------------------------------------------ - # Create subsets of all the observations - # ------------------------------------------------------ - # Now divide out observations by ACIS-S and ACIS-I - ACIS_I_obs, ACIS_S_obs, ACIS_hot_obs, sci_ecs_obs = acis_filter( - self.acis_and_ecs_obs, - ) - - temp = temps[self.name] - - # --------------------------------------------------------------- - # Planning - Collect any -84 C violations. These are load killers - # --------------------------------------------------------------- - - hi_viols = self._make_prediction_viols( - times, - temp, - load_start, - self.limits["planning_hi"].value, - "planning", - "max", - ) - viols = { - "hi": { - "name": f"Planning High ({self.limits['planning_hi'].value} C)", - "type": "Max", - "values": hi_viols, - }, - } - - acis_i_limit = self.limits["acis_i"].value - acis_s_limit = self.limits["acis_s"].value - acis_hot_limit = self.limits["acis_hot"].value - cold_ecs_limit = self.limits["cold_ecs"].value - - # ------------------------------------------------------------ - # ACIS-I - Collect any -112 C violations of any non-ECS ACIS-I - # science run. These are load killers - # ------------------------------------------------------------ - - mylog.info(f"ACIS-I Science ({acis_i_limit} C) violations") - - # Create the violation data structure. - acis_i_viols = self.search_obsids_for_viols( - "ACIS-I", - acis_i_limit, - ACIS_I_obs, - temp, - times, - load_start, - ) - - viols["ACIS_I"] = { - "name": f"ACIS-I ({acis_i_limit} C)", - "type": "Max", - "values": acis_i_viols, - } - - # ------------------------------------------------------------ - # ACIS-S - Collect any -111 C violations of any non-ECS ACIS-S - # science run. These are load killers - # ------------------------------------------------------------ - - mylog.info(f"ACIS-S Science ({acis_s_limit} C) violations") - - acis_s_viols = self.search_obsids_for_viols( - "ACIS-S", - acis_s_limit, - ACIS_S_obs, - temp, - times, - load_start, - ) - viols["ACIS_S"] = { - "name": f"ACIS-S ({acis_s_limit} C)", - "type": "Max", - "values": acis_s_viols, - } - - # ------------------------------------------------------------ - # ACIS-S Hot - Collect any -109 C violations of any non-ECS ACIS-S - # science run which can run hot. These are load killers - # ------------------------------------------------------------ - # - mylog.info(f"ACIS-S Science ({acis_hot_limit} C) violations") - - acis_hot_viols = self.search_obsids_for_viols( - "Hot ACIS-S", - acis_hot_limit, - ACIS_hot_obs, - temp, - times, - load_start, - ) - viols["ACIS_S_hot"] = { - "name": f"ACIS-S Hot ({acis_hot_limit} C)", - "type": "Max", - "values": acis_hot_viols, - } - - # ------------------------------------------------------------ - # Science Orbit ECS -119.5 violations; -119.5 violation check - # ------------------------------------------------------------ - mylog.info(f"Science Orbit ECS ({cold_ecs_limit} C) violations") - - ecs_viols = self.search_obsids_for_viols( - "Science Orbit ECS", - cold_ecs_limit, - sci_ecs_obs, - temp, - times, - load_start, - ) - - viols["ecs"] = { - "name": f"Science Orbit ECS ({cold_ecs_limit} C)", - "type": "Min", - "values": ecs_viols, - } - - # Store all obsids which can go to -109 C - for obs in ACIS_hot_obs: - self.acis_hot_obs.append(obs) - - return viols - - def search_obsids_for_viols( - self, - limit_name, - limit, - observations, - temp, - times, - load_start, - ): - """ - Given a planning limit and a list of observations, find those time intervals - where the temp gets warmer than the planning limit and identify which - observations (if any) include part or all of those intervals. - """ - viols_list = [] - - # Run through all observations - for eachobs in observations: - # Get the observation start science and stop science times, and obsid - obs_tstart = eachobs["start_science"] - obs_tstop = eachobs["tstop"] - # If the observation is in this load, let's look at it - if obs_tstart > load_start: - idxs = (times >= obs_tstart) & (times <= obs_tstop) - viols = self._make_prediction_viols( - times[idxs], - temp[idxs], - load_start, - limit, - limit_name, - "max", - ) - # If we have flagged any violations, record the obsid for each - # and add them to the list - if len(viols) > 0: - for viol in viols: - viol["obsid"] = str(eachobs["obsid"]) - viols_list += viols - - # Finished - return the violations list - return viols_list + # Store the obsid table + obs_table = self.limit_object.acis_obs_info.as_table() + # use only the obsids after the load start + idxs = np.where(CxoTime(obs_table["start_science"]).secs > load_start)[0] + self.acis_and_ecs_obs = obs_table[idxs] + # for each violation, add the exposure time to the violation + # so we can record it on the page + for v in viols["hi"]: + idx = np.where(self.acis_and_ecs_obs["obsid"] == v["obsid"])[0] + if idx.size == 0: + continue + row = self.acis_and_ecs_obs[idx[0]] + start_science = CxoTime(row["start_science"]).secs + row["bias_time"] + stop_science = CxoTime(row["stop_science"]).secs + v["exp_time"] = (stop_science - start_science) * 1.0e-3 + return viols, upper_limit, lower_limit def write_temps(self, outdir, times, temps): """ @@ -557,7 +369,6 @@ def draw_obsids( obsid = eachobservation["obsid"] in_fp = eachobservation["instrument"] - hot_acis = eachobservation["hot_acis"] if obsid > 60000: # ECS observations during the science orbit are colored blue @@ -568,7 +379,7 @@ def draw_obsids( if in_fp == "ACIS-I": color = "red" else: - color = "saddlebrown" if hot_acis else "green" + color = "green" obsid_txt = str(obsid) # If this is an ECS measurement in the science orbit mark @@ -577,8 +388,10 @@ def draw_obsids( obsid_txt += " (ECS)" # Convert the start and stop times into the Ska-required format - obs_start = cxctime2plotdate([eachobservation["tstart"]]) - obs_stop = cxctime2plotdate([eachobservation["tstop"]]) + tstart, tstop = CxoTime( + [eachobservation["start_science"], eachobservation["stop_science"]], + ) + obs_start, obs_stop = cxctime2plotdate([tstart, tstop]) if in_fp.startswith("ACIS-") or obsid > 60000: # For each ACIS Obsid, draw a horizontal line to show diff --git a/acis_thermal_check/apps/bep_pcb_check.py b/acis_thermal_check/apps/bep_pcb_check.py index d161aca..fae6f68 100755 --- a/acis_thermal_check/apps/bep_pcb_check.py +++ b/acis_thermal_check/apps/bep_pcb_check.py @@ -13,6 +13,7 @@ import sys import matplotlib +from chandra_limits import BEPPCBLimit from acis_thermal_check import DPABoardTempCheck, get_options @@ -23,12 +24,10 @@ class BEPPCBCheck(DPABoardTempCheck): + _limit_class = BEPPCBLimit + def __init__(self): - valid_limits = { - "TMP_BEP_PCB": [(1, 2.0), (50, 1.0), (99, 2.0)], - "PITCH": [(1, 3.0), (99, 3.0)], - "TSCPOS": [(1, 2.5), (99, 2.5)], - } + valid_limits = [(1, 2.0), (50, 1.0), (99, 2.0)] hist_limit = [20.0, 20.0] # First limit is >=, second limit is <= super().__init__("tmp_bep_pcb", "bep_pcb", valid_limits, hist_limit) diff --git a/acis_thermal_check/apps/cea_check.py b/acis_thermal_check/apps/cea_check.py index 864fbfb..bb21d83 100755 --- a/acis_thermal_check/apps/cea_check.py +++ b/acis_thermal_check/apps/cea_check.py @@ -14,9 +14,10 @@ import sys import matplotlib +from chandra_limits import CEALimit from ska_matplotlib import pointpair -from acis_thermal_check import ACISThermalCheck, get_options, mylog +from acis_thermal_check import ACISThermalCheck, get_options from acis_thermal_check.utils import PredictPlot # Matplotlib setup @@ -25,67 +26,21 @@ class CEACheck(ACISThermalCheck): + _limit_class = CEALimit + def __init__(self): - valid_limits = { - "2CEAHVPT": [(1, 2.0), (50, 1.0), (99, 2.0)], - "PITCH": [(1, 3.0), (99, 3.0)], - "TSCPOS": [(1, 2.5), (99, 2.5)], - } + valid_limits = [(1, 2.0), (50, 1.0), (99, 2.0)] hist_limit = [5.0] - limits_map = {} other_telem = ["2imonst", "2sponst", "2s2onst", "1dahtbon"] super().__init__( "2ceahvpt", "cea", valid_limits, hist_limit, - limits_map=limits_map, other_telem=other_telem, other_map={"1dahtbon": "dh_heater"}, ) - def make_prediction_viols(self, temps, states, load_start): - """ - Find limit violations where predicted temperature is above the - specified limits. - - Parameters - ---------- - temps : dict of NumPy arrays - NumPy arrays corresponding to the modeled temperatures - states : NumPy record array - Commanded states - load_start : float - The start time of the load, used so that we only report - violations for times later than this time for the model - run. - """ - mylog.info("Checking for limit violations") - - temp = temps[self.name] - times = self.predict_model.times - - # Only check this violation when HRC is on - mask = self.predict_model.comp["2imonst_on"].dvals - mask |= self.predict_model.comp["2sponst_on"].dvals - hi_viols = self._make_prediction_viols( - times, - temp, - load_start, - self.limits["planning_hi"].value, - "planning", - "max", - mask=mask, - ) - viols = { - "hi": { - "name": f"Hot ({self.limits['planning_hi'].value} C)", - "type": "Max", - "values": hi_viols, - }, - } - return viols - def _calc_model_supp(self, model, state_times, states, ephem, state0): """ Update to initialize the cea0 pseudo-node. If 2ceahvpt diff --git a/acis_thermal_check/apps/dea_check.py b/acis_thermal_check/apps/dea_check.py index 69c61b1..d8df9cb 100755 --- a/acis_thermal_check/apps/dea_check.py +++ b/acis_thermal_check/apps/dea_check.py @@ -15,6 +15,7 @@ import sys import matplotlib +from chandra_limits import DEALimit from acis_thermal_check import ACISThermalCheck, get_options @@ -25,12 +26,10 @@ class DEACheck(ACISThermalCheck): + _limit_class = DEALimit + def __init__(self): - valid_limits = { - "1DEAMZT": [(1, 2.0), (50, 1.0), (99, 2.0)], - "PITCH": [(1, 3.0), (99, 3.0)], - "TSCPOS": [(1, 2.5), (99, 2.5)], - } + valid_limits = [(1, 2.0), (50, 1.0), (99, 2.0)] hist_limit = [20.0] super().__init__("1deamzt", "dea", valid_limits, hist_limit) diff --git a/acis_thermal_check/apps/dpa_check.py b/acis_thermal_check/apps/dpa_check.py index 8a5ce0b..4da3e1d 100755 --- a/acis_thermal_check/apps/dpa_check.py +++ b/acis_thermal_check/apps/dpa_check.py @@ -13,6 +13,7 @@ import sys import matplotlib +from chandra_limits import DPALimit from acis_thermal_check import ACISThermalCheck, get_options @@ -22,88 +23,17 @@ class DPACheck(ACISThermalCheck): + _limit_class = DPALimit + _flag_cold_viols = True + def __init__(self): - valid_limits = { - "1DPAMZT": [(1, 2.0), (50, 1.0), (99, 2.0)], - "PITCH": [(1, 3.0), (99, 3.0)], - "TSCPOS": [(1, 2.5), (99, 2.5)], - } + valid_limits = [(1, 2.0), (50, 1.0), (99, 2.0)] hist_limit = [20.0] - limits_map = {"planning.caution.low": "zero_feps"} super().__init__( "1dpamzt", "dpa", valid_limits, hist_limit, - limits_map=limits_map, - ) - - def custom_prediction_viols(self, times, temp, viols, load_start): - """ - Custom handling of limit violations. This is for checking the - +12 degC violation if all FEPs are off. - - Parameters - ---------- - times : NumPy array - The times for the predicted temperatures - temp : NumPy array - The predicted temperatures - viols : dict - Dictionary of violations information to add to - load_start : float - The start time of the load, used so that we only report - violations for times later than this time for the model - run. - """ - # Only check this violation when all FEPs are off - mask = self.predict_model.comp["fep_count"].dvals == 0 - zf_viols = self._make_prediction_viols( - times, - temp, - load_start, - self.limits["zero_feps"].value, - "zero-feps", - "min", - mask=mask, - ) - viols["zero_feps"] = { - "name": f"Zero FEPs ({self.limits['zero_feps'].value} C)", - "type": "Min", - "values": zf_viols, - } - - def custom_prediction_plots(self, plots): - """ - Customization of prediction plots. - - Parameters - ---------- - plots : dict of dicts - Contains the hooks to the plot figures, axes, and filenames - and can be used to customize plots before they are written, - e.g. add limit lines, etc. - """ - plots[self.name].add_limit_line(self.limits["zero_feps"], "Zero FEPs", ls="--") - - def custom_validation_plots(self, plots): - """ - Customization of validation plots. - - Parameters - ---------- - plots : dict of dicts - Contains the hooks to the plot figures, axes, and filenames - and can be used to customize plots before they are written, - e.g. add limit lines, etc. - """ - plots["1dpamzt"]["lines"]["ax"].axhline( - self.limits["zero_feps"].value, - linestyle="--", - zorder=-8, - color=self.limits["zero_feps"].color, - linewidth=2, - label="Zero FEPs", ) def _calc_model_supp(self, model, state_times, states, ephem, state0): @@ -112,7 +42,7 @@ def _calc_model_supp(self, model, state_times, states, ephem, state0): has an initial value (T_dpa) - which it does at prediction time (gets it from state0), then T_dpa0 is set to that. If we are running the validation, - T_dpa is set to None so we use the dvals in model.comp + T_dpa is set to None, so we use the dvals in model.comp NOTE: If you change the name of the dpa0 pseudo node you have to edit the new name into the if statement diff --git a/acis_thermal_check/apps/fep1_actel_check.py b/acis_thermal_check/apps/fep1_actel_check.py index c302e6f..04fc4fe 100755 --- a/acis_thermal_check/apps/fep1_actel_check.py +++ b/acis_thermal_check/apps/fep1_actel_check.py @@ -14,6 +14,7 @@ import sys import matplotlib +from chandra_limits import FEP1ActelLimit from acis_thermal_check import DPABoardTempCheck, get_options @@ -23,12 +24,10 @@ class FEP1ActelCheck(DPABoardTempCheck): + _limit_class = FEP1ActelLimit + def __init__(self): - valid_limits = { - "TMP_FEP1_ACTEL": [(1, 2.0), (50, 1.0), (99, 2.0)], - "PITCH": [(1, 3.0), (99, 3.0)], - "TSCPOS": [(1, 2.5), (99, 2.5)], - } + valid_limits = [(1, 2.0), (50, 1.0), (99, 2.0)] hist_limit = [25.0, 20.0] # First limit is >=, second limit is <= super().__init__("tmp_fep1_actel", "fep1_actel", valid_limits, hist_limit) diff --git a/acis_thermal_check/apps/fep1_mong_check.py b/acis_thermal_check/apps/fep1_mong_check.py index 495a5ed..91de513 100755 --- a/acis_thermal_check/apps/fep1_mong_check.py +++ b/acis_thermal_check/apps/fep1_mong_check.py @@ -14,6 +14,7 @@ import sys import matplotlib +from chandra_limits import FEP1MongLimit from acis_thermal_check import DPABoardTempCheck, get_options @@ -24,12 +25,10 @@ class FEP1MongCheck(DPABoardTempCheck): + _limit_class = FEP1MongLimit + def __init__(self): - valid_limits = { - "TMP_FEP1_MONG": [(1, 2.0), (50, 1.0), (99, 2.0)], - "PITCH": [(1, 3.0), (99, 3.0)], - "TSCPOS": [(1, 2.5), (99, 2.5)], - } + valid_limits = [(1, 2.0), (50, 1.0), (99, 2.0)] hist_limit = [25.0, 20.0] # First limit is >=, second limit is <= super().__init__("tmp_fep1_mong", "fep1_mong", valid_limits, hist_limit) diff --git a/acis_thermal_check/apps/psmc_check.py b/acis_thermal_check/apps/psmc_check.py index 32b0b86..2e042af 100755 --- a/acis_thermal_check/apps/psmc_check.py +++ b/acis_thermal_check/apps/psmc_check.py @@ -13,6 +13,7 @@ import sys import matplotlib +from chandra_limits import PSMCLimit from acis_thermal_check import ACISThermalCheck, get_options @@ -23,12 +24,10 @@ class PSMCCheck(ACISThermalCheck): + _limit_class = PSMCLimit + def __init__(self): - valid_limits = { - "1PDEAAT": [(1, 2.5), (50, 1.0), (99, 5.5)], - "PITCH": [(1, 3.0), (99, 3.0)], - "TSCPOS": [(1, 2.5), (99, 2.5)], - } + valid_limits = [(1, 2.5), (50, 1.0), (99, 5.5)] hist_limit = [30.0, 40.0] super().__init__( "1pdeaat", diff --git a/acis_thermal_check/main.py b/acis_thermal_check/main.py index 3381533..4ad9375 100644 --- a/acis_thermal_check/main.py +++ b/acis_thermal_check/main.py @@ -14,6 +14,7 @@ import ska_numpy from astropy.io import ascii from astropy.table import Table +from chandra_limits import determine_obsid_info from cxotime import CxoTime from kadi import events from ska_matplotlib import cxctime2plotdate, plot_cxctime, pointpair @@ -25,7 +26,6 @@ PredictPlot, calc_pitch_roll, config_logging, - get_acis_limits, make_state_builder, mylog, paint_perigee, @@ -43,6 +43,8 @@ class ACISThermalCheck: + _limit_class = None + _flag_cold_viols = False r""" ACISThermalCheck class for making thermal model predictions and validating past model data against telemetry @@ -90,9 +92,6 @@ class ACISThermalCheck: internally, so this dict is for "extra" ones such as the 0-FEPs +12 C limit for 1DPAMZT. Default: None, meaning no extra limits are specified. - flag_cold_viols : boolean, optional - If set, violations for the lower planning limit will be - checked for and flagged, and hist_ops : list of strings, optional This sets the operations which will be used to create the error histograms, e.g., including only temperatures above @@ -112,30 +111,21 @@ def __init__( hist_limit, other_telem=None, other_map=None, - limits_map=None, - flag_cold_viols=False, hist_ops=None, ): self.msid = msid self.name = name - self.validation_limits = validation_limits + self.validation_limits = { + msid.upper(): validation_limits, + "PITCH": [(1, 3.0), (99, 3.0)], + "TSCPOS": [(1, 2.5), (99, 2.5)], + } self.hist_limit = hist_limit self.other_telem = other_telem self.other_map = other_map - self.limits_map = { - "odb.caution.high": "yellow_hi", - "odb.caution.low": "yellow_lo", - "safety.caution.high": "yellow_hi", - "safety.caution.low": "yellow_lo", - "planning.warning.high": "planning_hi", - "planning.warning.low": "planning_lo", - } - if limits_map is not None: - self.limits_map.update(limits_map) # Initially, the state_builder is set to None, as it will get # set up later self.state_builder = None - self.flag_cold_viols = flag_cold_viols if hist_ops is None: hist_ops = ["greater_equal"] * len(hist_limit) self.hist_ops = hist_ops @@ -168,6 +158,19 @@ def run(self, args, override_limits=None): proc, model_spec = self._setup_proc_and_logger(args) + # This allows one to override the limits for a particular model + # run. THIS SHOULD ONLY BE USED FOR TESTING PURPOSES. + if override_limits is not None: + for k, v in override_limits.items(): + if k in model_spec["limits"][self.msid]: + limit = model_spec["limits"][self.msid][k] + mylog.warning("Replacing %s %.2f with %.2f", k, limit, v) + model_spec["limits"][self.msid][k] = v + + # Set up the limit object and limits + self.limit_object = self._limit_class(model_spec=model_spec, margin=0.0) + self.limits = self.limit_object.limits + # Record the selected state builder in the class attributes # If there is no "state_builder" command line argument assume # kadi @@ -184,17 +187,6 @@ def run(self, args, override_limits=None): # data to a pickle later self.write_pickle = args.run_start is not None - self.limits = get_acis_limits(self.msid, model_spec, limits_map=self.limits_map) - - # This allows one to override the limits for a particular model - # run. THIS SHOULD ONLY BE USED FOR TESTING PURPOSES. - if override_limits is not None: - for k, v in override_limits.items(): - if k in self.limits: - limit = self.limits[k].value - mylog.warning(f"Replacing {k} {limit:.2f} with {v:.2f}") - self.limits[k].value = v - # Determine the start and stop times either from whatever was # stored in state_builder or punt by using NOW and None for # tstart and tstop. @@ -245,7 +237,7 @@ def run(self, args, override_limits=None): plots_validation = defaultdict(lambda: None) if pred["viols"] is not None: - any_viols = sum(len(viol["values"]) for viol in pred["viols"].values()) + any_viols = sum(len(viol) for viol in pred["viols"].values()) else: any_viols = 0 @@ -263,8 +255,6 @@ def run(self, args, override_limits=None): "pred_only": args.pred_only, "plots_validation": plots_validation, } - if self.msid == "fptemp": - context["acis_hot_obs"] = self.acis_hot_obs self.write_index_rst(args.outdir, context) @@ -345,6 +335,13 @@ def make_week_predict(self, tstart, tstop, tlm, T_init, model_spec, outdir): # Get commanded states and set initial temperature states, state0 = self.get_states(tlm, T_init) + # We have the states, and at this point the ACIS FP model + # needs to know the times of the observations to construct + # the limit later. + if self.msid == "fptemp": + obs_list = determine_obsid_info(states) + self.limit_object.set_obs_info(obs_list) + # calc_model actually does the model calculation by running # model-specific code. model = self.calc_model( @@ -369,11 +366,15 @@ def make_week_predict(self, tstart, tstop, tlm, T_init, model_spec, outdir): temps = {self.name: model.comp[self.msid].mvals} # make_prediction_viols determines the violations and prints them out - viols = self.make_prediction_viols(temps, states, tstart) + viols, upper_limit, lower_limit = self.make_prediction_viols( + temps, states, tstart + ) # make_prediction_plots runs the validation of the model # against previous telemetry - plots = self.make_prediction_plots(outdir, states, temps, tstart) + plots = self.make_prediction_plots( + outdir, states, temps, tstart, upper_limit, lower_limit + ) # write_states writes the commanded states to states.dat self.write_states(outdir, states) @@ -496,65 +497,6 @@ def make_validation_viols(self, plots_validation): return viols - def _make_prediction_viols( - self, - times, - temp, - load_start, - limit, - lim_name, - lim_type, - mask=None, - ): - if mask is None: - mask = np.ones_like(temp, dtype="bool") - viols = [] - if lim_type == "min": - bad = (temp <= limit) & mask - elif lim_type == "max": - bad = (temp >= limit) & mask - op = getattr(np, lim_type) - # The NumPy black magic of the next two lines is to figure - # out which time periods have planning limit violations and - # to find the bounding indexes of these times. This will also - # find violations which happen for one discrete time value also. - bad = np.concatenate(([False], bad, [False])) - changes = np.flatnonzero(bad[1:] != bad[:-1]).reshape(-1, 2) - # Now go through the periods where the temperature violates - # the planning limit and flag the duration and maximum of - # the violation - for change in changes: - # Only report violations which occur after the load being - # reviewed starts. - in_load = times[change[0]] > load_start or ( - times[change[0]] < load_start < times[change[1]] - ) - if times[change[0]] > load_start: - tstart = times[change[0]] - else: - tstart = load_start - tstop = times[change[1] - 1] - datestart = CxoTime(tstart).date - datestop = CxoTime(tstop).date - duration = tstop - tstart - # Only count the violation if it's in the load - # and if the duration is more than 10s - if in_load and duration >= 10.0: - viol = { - "datestart": datestart, - "datestop": datestop, - "duration": duration * 1.0e-3, - "extemp": op(temp[change[0] : change[1]]), - } - mylog.info( - f"WARNING: {self.msid} violates {lim_name} limit " - + "of %.2f degC from %s to %s" - % (limit, viol["datestart"], viol["datestop"]), - ) - viols.append(viol) - - return viols - def make_prediction_viols(self, temps, states, load_start): """ Find limit violations where predicted temperature is above the @@ -573,64 +515,33 @@ def make_prediction_viols(self, temps, states, load_start): """ mylog.info("Checking for limit violations") - temp = temps[self.name] - times = self.predict_model.times - - hi_viols = self._make_prediction_viols( - times, - temp, - load_start, - self.limits["planning_hi"].value, - "planning", - "max", + # Get the upper planning limit line + upper_limit = self.limit_object.get_limit_line( + states, + which="high", ) + # Check the violations for the upper planning limit viols = { - "hi": { - "name": f"Hot ({self.limits['planning_hi'].value} C)", - "type": "Max", - "values": hi_viols, - }, + "hi": upper_limit.check_violations( + self.predict_model, + start_time=load_start, + ), } - - if self.flag_cold_viols: - lo_viols = self._make_prediction_viols( - times, - temp, - load_start, - self.limits["planning_lo"].value, - "planning", - "min", + if self._flag_cold_viols: + # Get the lower planning limit line + lower_limit = self.limit_object.get_limit_line( + states, + which="low", ) - viols["lo"] = { - "name": f"Cold ({self.limits['planning_lo'].value} C)", - "type": "Min", - "values": lo_viols, - } - - # Handle any additional violations one wants to check, - # can be overridden by a subclass - self.custom_prediction_viols(times, temp, viols, load_start) - - return viols - - def custom_prediction_viols(self, times, temp, viols, load_start): - """ - This method is here to allow a subclass - to handle its own violations. + # Check the violations for the lower planning limit + viols["lo"] = lower_limit.check_violations( + self.predict_model, + start_time=load_start, + ) + else: + lower_limit = None - Parameters - ---------- - times : NumPy array - The times for the predicted temperatures - temp : NumPy array - The predicted temperatures - viols : dict - Dictionary of violations information to add to - load_start : float - The start time of the load, used so that we only report - violations for times later than this time for the model - run. - """ + return viols, upper_limit, lower_limit def write_states(self, outdir, states): """ @@ -765,7 +676,9 @@ def _make_state_plots(self, plots, num_figs, w1, plot_start, states, load_start) ) plots[plt_name].filename = f"{plt_name}.png" - def make_prediction_plots(self, outdir, states, temps, load_start): + def make_prediction_plots( + self, outdir, states, temps, load_start, upper_limit, lower_limit + ): """ Make plots of the thermal prediction as well as associated commanded states. @@ -809,16 +722,31 @@ def make_prediction_plots(self, outdir, states, temps, load_start): width=w1, load_start=load_start, ) - # Add horizontal lines for the planning and caution limits ymin, ymax = plots[self.name].ax.get_ylim() - ymax = max(self.limits["yellow_hi"].value + 1, ymax) + # Add horizontal lines for yellow limits, if necessary + for key in self.limit_object.alt_names.values(): + if key.startswith("yellow"): + plots[self.name].add_limit_line(self.limits[key]) + ymax = max(self.limits[key]["value"] + 1, ymax) + ymin = min(self.limits[key]["value"] - 1, ymin) + # Plot lines for upper and lower planning limits + upper_limit.plot( + fig_ax=(plots[self.name].fig, plots[self.name].ax), + lw=3, + zorder=2, + use_colors=True, + show_changes=False, + ) + if lower_limit is not None: + lower_limit.plot( + fig_ax=(plots[self.name].fig, plots[self.name].ax), + lw=3, + zorder=2, + use_colors=True, + show_changes=False, + no_label=self.msid != "1dpamzt", + ) plots[self.name].ax.set_title(self.msid.upper(), loc="left", pad=10) - plots[self.name].add_limit_line(self.limits["yellow_hi"], "Yellow") - plots[self.name].add_limit_line(self.limits["planning_hi"], "Planning") - if self.flag_cold_viols: - ymin = min(self.limits["yellow_lo"].value - 1, ymin) - plots[self.name].add_limit_line(self.limits["yellow_lo"], None) - plots[self.name].add_limit_line(self.limits["planning_lo"], None) plots[self.name].ax.set_ylim(ymin, ymax) plots[self.name].filename = self.msid.lower() + ".png" @@ -830,10 +758,6 @@ def make_prediction_plots(self, outdir, states, temps, load_start): plots["default"] = plots[self.name] - # This call allows the specific check tool - # to customize plots after the fact - self.custom_prediction_plots(plots) - # Make the legend on the temperature plot # only now after we've allowed for # customizations @@ -858,18 +782,6 @@ def make_prediction_plots(self, outdir, states, temps, load_start): return plots - def custom_prediction_plots(self, plots): - """ - Customization of prediction plots. - - Parameters - ---------- - plots : dict of dicts - Contains the hooks to the plot figures, axes, and filenames - and can be used to customize plots before they are written, - e.g. add limit lines, etc. - """ - def get_histogram_mask(self, tlm, limits): """ This method determines which values of telemetry @@ -918,6 +830,13 @@ def make_validation_plots(self, tlm, model_spec, outdir): stop = tlm["date"][-1] states = self.state_builder.get_validation_states(start, stop) + # We have the states, and at this point the ACIS FP model + # needs to know the times of the observations to construct + # the limit later. + if self.msid == "fptemp": + obs_list = determine_obsid_info(states) + self.limit_object.set_obs_info(obs_list) + mylog.info("Calculating %s thermal model for validation", self.name.upper()) # Run the thermal model from the beginning of obtained telemetry @@ -926,6 +845,20 @@ def make_validation_plots(self, tlm, model_spec, outdir): self.validate_model = model + # get the upper planning limit + upper_limit = self.limit_object.get_limit_line( + states, + which="high", + ) + if self._flag_cold_viols: + # get the lower planning limit + lower_limit = self.limit_object.get_limit_line( + states, + which="low", + ) + else: + lower_limit = None + # Use an OrderedDict here because we want the plots on the validation # page to appear in this order pred = OrderedDict( @@ -1019,7 +952,7 @@ def make_validation_plots(self, tlm, model_spec, outdir): fmt=".c", zorder=10, ) - ax.set_title(msid.upper() + " validation", loc="left", pad=10) + ax.set_title(msid.upper() + " validation", loc="left", pad=10, fontsize=15) ax.set_xlabel("Date") ax.set_ylabel(labels[msid]) ax.grid() @@ -1029,77 +962,47 @@ def make_validation_plots(self, tlm, model_spec, outdir): ptimes = cxctime2plotdate([rz.tstart, rz.tstop]) for ptime in ptimes: ax.axvline(ptime, ls="--", color="C2", linewidth=2, zorder=2) - # Add horizontal lines for the planning and caution limits - # or the limits for the focal plane model. Make sure we can - # see all of the limits. + # Add lines for all the limits and make sure we can see the + # lines by adjusting ymin/ymax accordingly. if self.msid == msid: ymin, ymax = ax.get_ylim() - if msid == "fptemp": - ax.axhline( - self.limits["cold_ecs"].value, - linestyle="--", - label="Cold ECS", - color=self.limits["cold_ecs"].color, - zorder=2, - linewidth=2, - ) - ax.axhline( - self.limits["acis_i"].value, - linestyle="--", - label="ACIS-I", - color=self.limits["acis_i"].color, - zorder=2, - linewidth=2, - ) - ax.axhline( - self.limits["acis_s"].value, - linestyle="--", - label="ACIS-S", - color=self.limits["acis_s"].color, - zorder=2, - linewidth=2, - ) - ax.axhline( - self.limits["acis_hot"].value, - linestyle="--", - label="Hot ACIS", - color=self.limits["acis_hot"].color, - zorder=2, - linewidth=2, - ) - ymax = max(self.limits["acis_hot"].value + 1, ymax) - else: - ax.axhline( - self.limits["yellow_hi"].value, - linestyle="-", - linewidth=2, - zorder=2, - color=self.limits["yellow_hi"].color, - ) - ax.axhline( - self.limits["planning_hi"].value, - linestyle="-", - linewidth=2, + # Plot the upper planning limit + upper_limit.plot( + fig_ax=(fig, ax), + lw=3, + zorder=2, + use_colors=True, + show_changes=False, + ) + if lower_limit is not None: + # Plot the lower planning limit + lower_limit.plot( + fig_ax=(fig, ax), + lw=3, zorder=2, - color=self.limits["planning_hi"].color, + use_colors=True, + show_changes=False, + no_label=self.msid != "1dpamzt", ) - ymax = max(self.limits["yellow_hi"].value + 1, ymax) - if self.flag_cold_viols: + # Add horizontal lines for yellow limits, if necessary + label = "Yellow" + for key in self.limit_object.alt_names.values(): + if key.startswith("yellow"): ax.axhline( - self.limits["yellow_lo"].value, + self.limits[key]["value"], linestyle="-", linewidth=2, zorder=2, - color=self.limits["yellow_lo"].color, + color=self.limits[key]["color"], + label=label, ) - ax.axhline( - self.limits["planning_lo"].value, - linestyle="-", - linewidth=2, - zorder=2, - color=self.limits["planning_lo"].color, - ) - ymin = min(self.limits["yellow_lo"].value - 1, ymin) + # Set label to None so we don't repeat it + label = None + # ymin and ymax for plots have to be at least + # above/below their yellow limits or the data + # extremes + ymax = max(self.limits[key]["value"] + 1, ymax) + ymin = min(self.limits[key]["value"] - 1, ymin) ax.set_ylim(ymin, ymax) ax.set_xlim(xmin, xmax) @@ -1272,19 +1175,16 @@ def make_validation_plots(self, tlm, model_spec, outdir): fig_id += 1 - # This call allows the specific check tool - # to customize plots after the fact - self.custom_validation_plots(plots) - if self.msid == "fptemp": - anchor = (0.295, 0.99) + anchor = (0.21, 0.99) else: anchor = (0.4, 0.99) plots[self.msid]["lines"]["ax"].legend( bbox_to_anchor=anchor, loc="lower left", - ncol=3, - fontsize=14, + ncol=5 if self.msid == "fptemp" else 3, + fontsize=12 if self.msid == "fptemp" else 14, + columnspacing=1.5, ) # Now write all of the plots after possible @@ -1316,18 +1216,6 @@ def make_validation_plots(self, tlm, model_spec, outdir): return plots - def custom_validation_plots(self, plots): - """ - Customization of prediction plots. - - Parameters - ---------- - plots : dict of dicts - Contains the hooks to the plot figures, axes, and filenames - and can be used to customize plots before they are written, - e.g. add limit lines, etc. - """ - def rst_to_html(self, outdir): """ Render index.rst as HTML @@ -1351,22 +1239,27 @@ def rst_to_html(self, outdir): ) stylesheet_path = str(outdir / "acis_thermal_check.css") - infile = str(outdir / "index.rst") - outfile = str(outdir / "index.html") - publish_file( - source_path=infile, - destination_path=outfile, - writer_name="html", - settings_overrides={"stylesheet_path": stylesheet_path}, - ) + prefixes = ["index"] + if self.msid == "fptemp": + # For the ACIS FP limit we write out an obsid table + prefixes.append("obsid_table") + for prefix in prefixes: + infile = str(outdir / f"{prefix}.rst") + outfile = str(outdir / f"{prefix}.html") + publish_file( + source_path=infile, + destination_path=outfile, + writer_name="html", + settings_overrides={"stylesheet_path": stylesheet_path}, + ) - # Remove the stupid field that docbook inserts. This - # prevents HTML table auto-sizing. - del_colgroup = re.compile(r".*?", re.DOTALL) - with open(outfile) as f: - outtext = del_colgroup.sub("", f.read()) - with open(outfile, "w") as f: - f.write(outtext) + # Remove the stupid field that docbook inserts. This + # prevents HTML table auto-sizing. + del_colgroup = re.compile(r".*?", re.DOTALL) + with open(outfile) as f: + outtext = del_colgroup.sub("", f.read()) + with open(outfile, "w") as f: + f.write(outtext) def write_index_rst(self, outdir, context): """ @@ -1393,6 +1286,23 @@ def write_index_rst(self, outdir, context): # Render the template and write it to a file with open(outfile, "w") as fout: fout.write(template.render(**context)) + if self.msid == "fptemp": + # For the ACIS FP limit we write out an obsid table + context["acis_obs"] = self.acis_and_ecs_obs + template_path = ( + TASK_DATA / "acis_thermal_check/templates/obsid_template.rst" + ) + outfile = outdir / "obsid_table.rst" + # Open up the reST template and send the context to it using jinja2 + with open(template_path) as fin: + index_template = fin.read() + index_template = re.sub(r" %}\n", " %}", index_template) + template = jinja2.Template(index_template) + # Render the template and write it to a file + with open(outfile, "w") as fout: + fout.write( + template.render(acis_obs=self.acis_and_ecs_obs, bsdir=self.bsdir) + ) def _setup_proc_and_logger(self, args): """ @@ -1549,6 +1459,8 @@ def get_telem_values(self, tstart, days=14): class DPABoardTempCheck(ACISThermalCheck): + _flag_cold_viols = True + def __init__( self, msid, @@ -1566,6 +1478,5 @@ def __init__( hist_limit, other_telem=other_telem, other_map=other_map, - flag_cold_viols=True, hist_ops=hist_ops, ) diff --git a/acis_thermal_check/templates/index_template.rst b/acis_thermal_check/templates/index_template.rst index f9e06d5..349fd33 100644 --- a/acis_thermal_check/templates/index_template.rst +++ b/acis_thermal_check/templates/index_template.rst @@ -1,5 +1,5 @@ ================================ -{{proc.name}} temperatures check +{{proc.name}} Temperatures Check ================================ .. role:: red @@ -32,43 +32,45 @@ States ``_ ===================== ============================================= {% if proc.msid == "FPTEMP" %} -"Hot" ACIS Observations (-109 C limit) --------------------------------------- -{% if acis_hot_obs|length > 0 %} -===== ================= ================== ======= ====== ================= -Obsid CCDs # of counts in seq Grating Cycle Spectra Max Count -===== ================= ================== ======= ====== ================= -{% for eachobs in acis_hot_obs %} -{{ eachobs.obsid }} {{"{0: <17}".format(eachobs.ccds)}} {{"{0: <18}".format(eachobs.num_counts)}} {{eachobs.grating}} {{"{0: <6}".format(eachobs.obs_cycle)}} {{"{0: <10}".format(eachobs.spectra_max_count)}} -{% endfor %} -===== ================= ================== ======= ====== ================= -{% endif %} +`ACIS Observations Table `_ {% endif %} -{% for key in viols.keys() %} +{% for key in viols %} -{% if viols[key]["values"]|length > 0 %} -{{proc.msid}} {{viols[key]["name"]}} Violations +{% if key == "hi" %} +{% set viol_type = "Upper Limit" %} +{% set extreme = "Max" %} +{% else %} +{% set viol_type = "Lower Limit" %} +{% set extreme = "Min" %} +{% endif %} + +{% if viols[key]|length > 0 %} +{{proc.msid}} {{viol_type}} Violations --------------------------------------------------- {% if proc.msid == "FPTEMP" %} -===================== ===================== ================= ================== ================== -Date start Date stop Duration (ks) Max temperature Obsids -===================== ===================== ================= ================== ================== -{% for viol in viols[key]["values"] %} -{{viol.datestart}} {{viol.datestop}} {{"{:3.2f}".format(viol.duration).rjust(8)}} {{"%.2f"|format(viol.extemp)}} {{viol.obsid}} +===================== ===================== ======================== ======================= ================== +Date start Date stop Duration / Exposure (ks) Max Temperature / Limit Obsid +===================== ===================== ======================== ======================= ================== +{% for viol in viols[key] %} +{% if viol.obsid == -1 %} +{{viol.datestart}} {{viol.datestop}} {{"{:3.2f}".format(viol.duration)}} / N/A {{"%.2f"|format(viol.extemp)}} / {{"%.2f"|format(viol.limit)}} N/A +{% else %} +{{viol.datestart}} {{viol.datestop}} {{"{:3.2f}".format(viol.duration)}} / {{"{:3.2f}".format(viol.exp_time)}} {{"%.2f"|format(viol.extemp)}} / {{"%.2f"|format(viol.limit)}} {{viol.obsid}} +{% endif %} {% endfor %} -===================== ===================== ================= ================== ================== +===================== ===================== ======================== ======================= ================== {% else %} -===================== ===================== ================= =================== -Date start Date stop Duration (ks) {{viols[key]["type"]}} Temperature -===================== ===================== ================= =================== -{% for viol in viols[key]["values"] %} -{{viol.datestart}} {{viol.datestop}} {{"{:3.2f}".format(viol.duration).rjust(8)}} {{"{:.2f}".format(viol.extemp)}} +===================== ===================== ================= =============================== +Date start Date stop Duration (ks) {{extreme}} Temperature / Limit +===================== ===================== ================= =============================== +{% for viol in viols[key] %} +{{viol.datestart}} {{viol.datestop}} {{"{:3.2f}".format(viol.duration).rjust(8)}} {{"{:.2f}".format(viol.extemp)}} / {{"{:.2f}".format(viol.limit)}} {% endfor %} -===================== ===================== ================= =================== +===================== ===================== ================= =============================== {% endif %} {% else %} -No {{proc.msid}} {{viols[key]["name"]}} Violations +No {{proc.msid}} {{viol_type}} Violations {% endif %} {% endfor %} diff --git a/acis_thermal_check/templates/obsid_template.rst b/acis_thermal_check/templates/obsid_template.rst new file mode 100644 index 0000000..a3a8e3f --- /dev/null +++ b/acis_thermal_check/templates/obsid_template.rst @@ -0,0 +1,12 @@ +ACIS Observations Table +----------------------- + +Observations in {{bsdir}} + +===== ================= ================== ======= ======== ================= ===== +Obsid CCDs # of counts in seq Grating SIM-Z Spectra Max Count Limit +===== ================= ================== ======= ======== ================= ===== +{% for eachobs in acis_obs %} +{{ eachobs.obsid }} {{"{0: <17}".format(eachobs.ccds)}} {{"{0: <18}".format(eachobs.num_counts)}} {{eachobs.grating}} {{eachobs.sim_z}} {{"{0: <10}".format(eachobs.spectra_max_count)}} {{eachobs.fp_limit}} +{% endfor %} +===== ================= ================== ======= ======== ================= ===== diff --git a/acis_thermal_check/tests/acisfp/acisfp_test_spec.json b/acis_thermal_check/tests/acisfp/acisfp_test_spec.json index 3e891ca..521e1a1 100644 --- a/acis_thermal_check/tests/acisfp/acisfp_test_spec.json +++ b/acis_thermal_check/tests/acisfp/acisfp_test_spec.json @@ -445,7 +445,8 @@ "fptemp": { "planning.data_quality.high.acisi": -112.0, "planning.data_quality.high.aciss": -111.0, - "planning.data_quality.high.aciss_hot": -109.0, + "planning.data_quality.high.aciss_hot": -109.0, + "planning.data_quality.high.aciss_hot_b": -105.0, "planning.data_quality.high.cold_ecs": -118.2, "planning.warning.high": -84.0, "safety.caution.high": -80.0, diff --git a/acis_thermal_check/tests/acisfp/answers/DEC0919A_viol.json b/acis_thermal_check/tests/acisfp/answers/DEC0919A_viol.json index aa3c26c..1a7359b 100644 --- a/acis_thermal_check/tests/acisfp/answers/DEC0919A_viol.json +++ b/acis_thermal_check/tests/acisfp/answers/DEC0919A_viol.json @@ -1,37 +1,49 @@ { "run_start": "2019:338:14:25:26.816", "limits": { - "acis_i": -115.0, - "acis_s": -113.0 + "planning.data_quality.high.acisi": -115.0, + "planning.data_quality.high.aciss": -113.0 }, "datestarts": [ - "2019:345:04:41:02.816", - "2019:346:17:40:30.816", - "2019:349:09:05:18.816", - "2019:343:04:40:06.816" + "2019:343:04:49:15.613", + "2019:345:05:00:28.506", + "2019:346:17:59:16.994", + "2019:349:09:26:27.484" ], "datestops": [ + "2019:343:07:35:55.613", "2019:345:08:47:02.816", "2019:346:21:24:38.816", - "2019:349:12:27:34.816", - "2019:343:07:35:02.816" + "2019:349:12:27:34.816" ], "temps": [ + "-111.28", "-112.16", "-112.86", - "-112.24", - "-111.28" + "-112.30" ], "obsids": [ + "23098", "23094", "22727", - "23096", - "23098" + "23096" ], "duration": [ - "14.76", - "13.45", - "12.14", - "10.50" + "10.00", + "13.59", + "12.32", + "10.87" + ], + "exposure": [ + "10.00", + "25.00", + "25.00", + "25.00" + ], + "limit": [ + "-113.00", + "-115.00", + "-115.00", + "-115.00" ] } \ No newline at end of file diff --git a/acis_thermal_check/tests/acisfp/answers/FEB2122A_acis_obsids.json b/acis_thermal_check/tests/acisfp/answers/FEB2122A_acis_obsids.json new file mode 100644 index 0000000..91b9bb0 --- /dev/null +++ b/acis_thermal_check/tests/acisfp/answers/FEB2122A_acis_obsids.json @@ -0,0 +1,308 @@ +[ + [ + "25103", + "S3", + "3000", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "26316", + "S2,S3", + "13800", + "NONE", + "5.85", + "0", + "-111.0" + ], + [ + "23862", + "S3", + "4500", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "24426", + "I0,I1,I2,I3,S3", + "4000", + "NONE", + "-4.095", + "2500", + "-112.0" + ], + [ + "24445", + "I0,I1,I2,I3,S2", + "1870", + "NONE", + "0.0", + "700", + "-109.0" + ], + [ + "26191", + "I3", + "100000", + "NONE", + "0.0", + "3000", + "-112.0" + ], + [ + "26337", + "I0,I1,I2,I3,S2", + "1870", + "NONE", + "0.0", + "700", + "-109.0" + ], + [ + "24244", + "S0,S1,S2,S3,S4,S5", + "765000", + "HETG", + "0.0", + "0", + "-109.0" + ], + [ + "24814", + "S2,S3,S4", + "4500", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "23585", + "I1,I3", + "81", + "NONE", + "0.0", + "100", + "-109.0" + ], + [ + "26338", + "I2,I3,S2,S3,S4", + "4500", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "26317", + "S2,S3", + "13800", + "NONE", + "5.85", + "0", + "-111.0" + ], + [ + "25858", + "S3", + "2700", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "23700", + "I3,S2,S3,S4", + "199", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "26312", + "I0,I1,I3", + "81", + "NONE", + "0.0", + "100", + "-109.0" + ], + [ + "24393", + "I2,I3,S2,S3,S4", + "9", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "26192", + "S3", + "100000", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "26333", + "S2,S3,S4", + "160", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "26336", + "I2,I3", + "4000", + "NONE", + "-4.095", + "2500", + "-112.0" + ], + [ + "26339", + "S3", + "2700", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "26318", + "S2,S3", + "13800", + "NONE", + "5.85", + "0", + "-111.0" + ], + [ + "26276", + "S2,S3,S4", + "225", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "25553", + "S2,S3,S4", + "3", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "25544", + "S2,S3,S4", + "3", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "26332", + "S3", + "1240", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "25456", + "I3,S2,S3,S4", + "0", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "25556", + "S3", + "3", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "26335", + "S1,S2,S3,S4", + "765000", + "HETG", + "0.0", + "0", + "-109.0" + ], + [ + "26334", + "S3", + "29", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "24373", + "I0,I1,I2,I3", + "30000", + "NONE", + "0.0", + "1000", + "-109.0" + ], + [ + "26319", + "S2,S3", + "13800", + "NONE", + "5.85", + "0", + "-111.0" + ], + [ + "25276", + "I2,I3,S1,S2,S3", + "10", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "24042", + "I0,I1,I2,I3", + "316", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "24043", + "I0,I1,I2,I3", + "316", + "NONE", + "0.0", + "20000", + "-112.0" + ] +] \ No newline at end of file diff --git a/acis_thermal_check/tests/acisfp/answers/FEB2122A_hot_acis.json b/acis_thermal_check/tests/acisfp/answers/FEB2122A_hot_acis.json deleted file mode 100644 index 4183d44..0000000 --- a/acis_thermal_check/tests/acisfp/answers/FEB2122A_hot_acis.json +++ /dev/null @@ -1,114 +0,0 @@ -[ - [ - "24244", - "S0,S1,S2,S3,S4,S5", - "765000", - "HETG", - "22", - "0" - ], - [ - "23585", - "I1,I3", - "81", - "NONE", - "22", - "100" - ], - [ - "23700", - "I3,S2,S3,S4", - "199", - "NONE", - "22", - "0" - ], - [ - "26312", - "I0,I1,I3", - "81", - "NONE", - "22", - "100" - ], - [ - "24393", - "I2,I3,S2,S3,S4", - "9", - "NONE", - "22", - "0" - ], - [ - "26333", - "S2,S3,S4", - "160", - "NONE", - "23", - "0" - ], - [ - "26276", - "S2,S3,S4", - "225", - "NONE", - "23", - "0" - ], - [ - "25553", - "S2,S3,S4", - "3", - "NONE", - "23", - "0" - ], - [ - "25544", - "S2,S3,S4", - "3", - "NONE", - "23", - "0" - ], - [ - "25456", - "I3,S2,S3,S4", - "0", - "NONE", - "23", - "0" - ], - [ - "25556", - "S3", - "3", - "NONE", - "23", - "0" - ], - [ - "26335", - "S1,S2,S3,S4", - "765000", - "HETG", - "22", - "0" - ], - [ - "26334", - "S3", - "29", - "NONE", - "23", - "0" - ], - [ - "25276", - "I2,I3,S1,S2,S3", - "10", - "NONE", - "23", - "0" - ] -] \ No newline at end of file diff --git a/acis_thermal_check/tests/acisfp/answers/FEB2822A_acis_obsids.json b/acis_thermal_check/tests/acisfp/answers/FEB2822A_acis_obsids.json new file mode 100644 index 0000000..7fc7cbe --- /dev/null +++ b/acis_thermal_check/tests/acisfp/answers/FEB2822A_acis_obsids.json @@ -0,0 +1,344 @@ +[ + [ + "23876", + "I0,I1,I2,I3,S2,S3", + "900", + "NONE", + "0.0", + "18000", + "-112.0" + ], + [ + "24813", + "I2,I3,S2,S3,S4", + "4500", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "25333", + "S3", + "5", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "26320", + "S2,S3", + "13800", + "NONE", + "14.625", + "0", + "-111.0" + ], + [ + "25141", + "I0,I1,I2,I3", + "135", + "NONE", + "0.0", + "300", + "-109.0" + ], + [ + "23653", + "S2,S3", + "260", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "26342", + "S2,S3,S4", + "160", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "26347", + "I2,I3,S2,S3,S4", + "4500", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "26348", + "I0,I1,I2,I3", + "135", + "NONE", + "0.0", + "300", + "-109.0" + ], + [ + "25656", + "S1,S2,S3,S4,S5", + "300000", + "LETG", + "-8.85", + "0", + "-109.0" + ], + [ + "26321", + "S2,S3", + "13800", + "NONE", + "14.625", + "0", + "-111.0" + ], + [ + "24705", + "S2,S3", + "500000", + "HETG", + "-12.1", + "0", + "-105.0" + ], + [ + "25657", + "S1,S2,S3", + "300000", + "LETG", + "-2.88", + "0", + "-109.0" + ], + [ + "26344", + "S2,S3", + "500000", + "HETG", + "-12.1", + "0", + "-105.0" + ], + [ + "26345", + "S2,S3", + "500000", + "HETG", + "-12.1", + "0", + "-105.0" + ], + [ + "25658", + "S1,S2,S3,S4,S5", + "300000", + "LETG", + "8.51", + "0", + "-109.0" + ], + [ + "26322", + "S2,S3", + "13800", + "NONE", + "14.625", + "0", + "-111.0" + ], + [ + "26349", + "I0,I1,I2,I3", + "135", + "NONE", + "0.0", + "300", + "-109.0" + ], + [ + "26311", + "S1,S2,S3,S4,S5", + "300000", + "LETG", + "8.51", + "0", + "-109.0" + ], + [ + "24108", + "I0,I1,I2,I3", + "259", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "24109", + "I0,I1,I2,I3", + "259", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "24110", + "I0,I1,I2,I3", + "259", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "24111", + "I0,I1,I2,I3", + "259", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "26309", + "S1,S2,S3,S4", + "300000", + "LETG", + "-8.85", + "0", + "-109.0" + ], + [ + "26310", + "S1,S2,S3", + "300000", + "LETG", + "-2.88", + "0", + "-109.0" + ], + [ + "24112", + "I0,I1,I2,I3", + "259", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "24113", + "I0,I1,I2,I3", + "259", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "26054", + "I0,I1,I2,I3,S2", + "2001", + "NONE", + "0.0", + "2000", + "-112.0" + ], + [ + "24808", + "S1,S2,S3,S4", + "27000", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "26341", + "S3", + "483", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "26346", + "S1,S2,S3,S4", + "27000", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "26340", + "S1,S2,S3,S4", + "443", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "26323", + "S2,S3", + "13800", + "NONE", + "14.625", + "0", + "-111.0" + ], + [ + "25291", + "S3", + "61", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "26038", + "I0,I1,I2,I3,S2,S3", + "10000", + "NONE", + "0.0", + "100", + "-109.0" + ], + [ + "23826", + "I2,I3,S1,S2,S3,S4", + "600", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "26035", + "I3,S1,S2,S3", + "130", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "26343", + "I2,I3,S1,S2,S3,S4", + "600", + "NONE", + "0.0", + "0", + "-111.0" + ] +] \ No newline at end of file diff --git a/acis_thermal_check/tests/acisfp/answers/FEB2822A_hot_acis.json b/acis_thermal_check/tests/acisfp/answers/FEB2822A_hot_acis.json deleted file mode 100644 index d2180b0..0000000 --- a/acis_thermal_check/tests/acisfp/answers/FEB2822A_hot_acis.json +++ /dev/null @@ -1,138 +0,0 @@ -[ - [ - "25333", - "S3", - "5", - "NONE", - "23", - "0" - ], - [ - "25141", - "I0,I1,I2,I3", - "135", - "NONE", - "23", - "300" - ], - [ - "23653", - "S2,S3", - "260", - "NONE", - "22", - "0" - ], - [ - "26342", - "S2,S3,S4", - "160", - "NONE", - "23", - "0" - ], - [ - "26348", - "I0,I1,I2,I3", - "135", - "NONE", - "23", - "300" - ], - [ - "24705", - "S2,S3", - "500000", - "HETG", - "22", - "0" - ], - [ - "26344", - "S2,S3", - "500000", - "HETG", - "22", - "0" - ], - [ - "26345", - "S2,S3", - "500000", - "HETG", - "22", - "0" - ], - [ - "26349", - "I0,I1,I2,I3", - "135", - "NONE", - "23", - "300" - ], - [ - "24108", - "I0,I1,I2,I3", - "259", - "NONE", - "22", - "20000" - ], - [ - "24109", - "I0,I1,I2,I3", - "259", - "NONE", - "22", - "20000" - ], - [ - "24110", - "I0,I1,I2,I3", - "259", - "NONE", - "22", - "20000" - ], - [ - "24111", - "I0,I1,I2,I3", - "259", - "NONE", - "22", - "20000" - ], - [ - "24112", - "I0,I1,I2,I3", - "259", - "NONE", - "22", - "20000" - ], - [ - "24113", - "I0,I1,I2,I3", - "259", - "NONE", - "22", - "20000" - ], - [ - "25291", - "S3", - "61", - "NONE", - "23", - "0" - ], - [ - "26035", - "I3,S1,S2,S3", - "130", - "NONE", - "23", - "0" - ] -] \ No newline at end of file diff --git a/acis_thermal_check/tests/acisfp/answers/JUN1421A_viol.json b/acis_thermal_check/tests/acisfp/answers/JUN1421A_viol.json index 9bc0e9b..a08f505 100644 --- a/acis_thermal_check/tests/acisfp/answers/JUN1421A_viol.json +++ b/acis_thermal_check/tests/acisfp/answers/JUN1421A_viol.json @@ -1,21 +1,27 @@ { "run_start": "2021:162:23:36:54.816", "limits": { - "cold_ecs": -121.0 + "planning.data_quality.high.cold_ecs": -121.0 }, "datestarts": [ - "2021:166:08:20:22.816" + "2021:166:08:38:47.582" ], "datestops": [ "2021:166:14:10:14.816" ], "duration": [ - "20.99" + "19.89" ], "temps": [ "-119.57" ], "obsids": [ "62633" + ], + "exposure": [ + "20.00" + ], + "limit": [ + "-121.00" ] } \ No newline at end of file diff --git a/acis_thermal_check/tests/acisfp/answers/MAR0722A_acis_obsids.json b/acis_thermal_check/tests/acisfp/answers/MAR0722A_acis_obsids.json new file mode 100644 index 0000000..42c1caa --- /dev/null +++ b/acis_thermal_check/tests/acisfp/answers/MAR0722A_acis_obsids.json @@ -0,0 +1,299 @@ +[ + [ + "23801", + "S2,S3,S4", + "640", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "25174", + "I0,I1,I2,I3", + "112", + "NONE", + "0.0", + "2000", + "-112.0" + ], + [ + "24283", + "S3", + "50", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "24186", + "I0,I1,I2,I3", + "259", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "24187", + "I0,I1,I2,I3", + "259", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "24188", + "I0,I1,I2,I3", + "259", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "26324", + "S2,S3", + "13800", + "NONE", + "14.625", + "0", + "-111.0" + ], + [ + "25404", + "S1,S2,S3,S4", + "50000", + "HETG", + "0.0", + "0", + "-109.0" + ], + [ + "25622", + "I3", + "350000", + "NONE", + "0.0", + "10000", + "-112.0" + ], + [ + "26351", + "S2,S3,S4", + "640", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "26359", + "I3", + "350000", + "NONE", + "0.0", + "10000", + "-112.0" + ], + [ + "26325", + "S2,S3", + "13800", + "NONE", + "10.2375", + "0", + "-111.0" + ], + [ + "24820", + "I0,I1,I2,I3", + "30000", + "NONE", + "0.0", + "1000", + "-109.0" + ], + [ + "25311", + "I2,I3,S1,S2,S3,S4", + "600", + "NONE", + "0.0", + "0", + "-111.0" + ], + [ + "25175", + "I0,I1,I2,I3", + "112", + "NONE", + "0.0", + "2000", + "-112.0" + ], + [ + "25621", + "I3", + "300000", + "NONE", + "0.0", + "10000", + "-112.0" + ], + [ + "25482", + "I0,I1,I2,I3,S2", + "2011", + "NONE", + "0.0", + "2000", + "-112.0" + ], + [ + "25454", + "I2,I3,S1,S2,S3,S4", + "0", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "24358", + "I0,I1,I2,I3", + "30000", + "NONE", + "0.0", + "1000", + "-109.0" + ], + [ + "26356", + "S0,S1,S2,S3,S4,S5", + "50000", + "HETG", + "0.0", + "0", + "-109.0" + ], + [ + "26326", + "S2,S3", + "13800", + "NONE", + "10.2375", + "0", + "-111.0" + ], + [ + "26352", + "S3", + "50", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "26353", + "I0,I1,I2,I3", + "30000", + "NONE", + "0.0", + "1000", + "-109.0" + ], + [ + "24438", + "I0,I1,I2,I3", + "10", + "NONE", + "0.0", + "1500", + "-112.0" + ], + [ + "26355", + "I0,I1,I2,I3", + "30000", + "NONE", + "0.0", + "1000", + "-109.0" + ], + [ + "24189", + "I0,I1,I2,I3", + "259", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "24190", + "I0,I1,I2,I3", + "259", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "24191", + "I0,I1,I2,I3", + "259", + "NONE", + "0.0", + "20000", + "-112.0" + ], + [ + "26354", + "I0,I1,I2,I3", + "10", + "NONE", + "0.0", + "1500", + "-112.0" + ], + [ + "26358", + "I3", + "300000", + "NONE", + "0.0", + "10000", + "-112.0" + ], + [ + "26357", + "I2,I3,S1,S2,S3,S4", + "0", + "NONE", + "0.0", + "0", + "-109.0" + ], + [ + "26327", + "S2,S3", + "13800", + "NONE", + "-5.85", + "0", + "-111.0" + ], + [ + "25266", + "S3", + "16", + "NONE", + "0.0", + "0", + "-109.0" + ] +] \ No newline at end of file diff --git a/acis_thermal_check/tests/acisfp/answers/MAR0722A_hot_acis.json b/acis_thermal_check/tests/acisfp/answers/MAR0722A_hot_acis.json deleted file mode 100644 index 7bec744..0000000 --- a/acis_thermal_check/tests/acisfp/answers/MAR0722A_hot_acis.json +++ /dev/null @@ -1,138 +0,0 @@ -[ - [ - "25174", - "I0,I1,I2,I3", - "112", - "NONE", - "23", - "2000" - ], - [ - "24283", - "S3", - "50", - "NONE", - "22", - "0" - ], - [ - "24186", - "I0,I1,I2,I3", - "259", - "NONE", - "22", - "20000" - ], - [ - "24187", - "I0,I1,I2,I3", - "259", - "NONE", - "22", - "20000" - ], - [ - "24188", - "I0,I1,I2,I3", - "259", - "NONE", - "22", - "20000" - ], - [ - "25404", - "S1,S2,S3,S4", - "50000", - "HETG", - "23", - "0" - ], - [ - "25175", - "I0,I1,I2,I3", - "112", - "NONE", - "23", - "2000" - ], - [ - "25454", - "I2,I3,S1,S2,S3,S4", - "0", - "NONE", - "23", - "0" - ], - [ - "26356", - "S0,S1,S2,S3,S4,S5", - "50000", - "HETG", - "23", - "0" - ], - [ - "26352", - "S3", - "50", - "NONE", - "22", - "0" - ], - [ - "24438", - "I0,I1,I2,I3", - "10", - "NONE", - "22", - "1500" - ], - [ - "24189", - "I0,I1,I2,I3", - "259", - "NONE", - "22", - "20000" - ], - [ - "24190", - "I0,I1,I2,I3", - "259", - "NONE", - "22", - "20000" - ], - [ - "24191", - "I0,I1,I2,I3", - "259", - "NONE", - "22", - "20000" - ], - [ - "26354", - "I0,I1,I2,I3", - "10", - "NONE", - "22", - "1500" - ], - [ - "26357", - "I2,I3,S1,S2,S3,S4", - "0", - "NONE", - "23", - "0" - ], - [ - "25266", - "S3", - "16", - "NONE", - "23", - "0" - ] -] \ No newline at end of file diff --git a/acis_thermal_check/tests/acisfp/answers/MAY0922A_viol.json b/acis_thermal_check/tests/acisfp/answers/MAY0922A_viol.json index 0935ad2..349fe2e 100644 --- a/acis_thermal_check/tests/acisfp/answers/MAY0922A_viol.json +++ b/acis_thermal_check/tests/acisfp/answers/MAY0922A_viol.json @@ -1,56 +1,62 @@ { "run_start": "2022:251:23:10:06.816", "limits": { - "planning_hi": -97.0 + "planning.warning.high": -97.0 }, "datestarts": [ "2022:128:08:49:58.816", + "2022:129:04:30:21.022", + "2022:130:07:27:35.009", "2022:131:00:09:18.816", - "2022:133:15:55:58.816", - "2022:130:07:12:30.816", - "2022:134:10:03:50.816", - "2022:134:21:54:30.816", - "2022:129:04:19:50.816", - "2022:132:11:03:02.816" + "2022:132:11:03:02.816", + "2022:133:15:55:58.816" ], "datestops": [ "2022:128:10:06:30.816", - "2022:131:01:47:42.816", - "2022:133:16:28:46.816", - "2022:130:09:07:18.816", - "2022:134:12:04:06.816", - "2022:134:22:43:42.816", "2022:129:05:58:14.816", - "2022:132:22:15:26.816" + "2022:130:09:07:18.816", + "2022:131:01:47:42.816", + "2022:132:22:19:07.462", + "2022:133:16:28:46.816" ], "duration": [ "4.59", + "5.27", + "5.98", "5.90", - "1.97", - "6.89", - "7.22", - "2.95", - "5.90", - "40.34" + "40.56", + "1.97" ], "temps": [ "-90.27", - "-88.14", - "-94.38", - "-111.44", - "-111.42", - "-111.87", "-110.62", - "-109.93" + "-111.44", + "-88.14", + "-109.93", + "-94.38" ], "obsids": [ - "", - "", - "", - "23816", - "26081", - "26418", + "N/A", "23827", - "23867" + "23816", + "N/A", + "23867", + "N/A" + ], + "exposure": [ + "N/A", + "22.00", + "16.00", + "N/A", + "43.00", + "N/A" + ], + "limit": [ + "-97.00", + "-111.00", + "-112.00", + "-97.00", + "-111.00", + "-97.00" ] } \ No newline at end of file diff --git a/acis_thermal_check/tests/acisfp/answers/SEP1321A_viol.json b/acis_thermal_check/tests/acisfp/answers/SEP1321A_viol.json index ae4a61a..476e9f2 100644 --- a/acis_thermal_check/tests/acisfp/answers/SEP1321A_viol.json +++ b/acis_thermal_check/tests/acisfp/answers/SEP1321A_viol.json @@ -1,31 +1,34 @@ { "run_start": "2021:251:23:10:06.816", "limits": { - "acis_hot": -110.0 + "planning.data_quality.high.aciss_hot": -110.0 }, "datestarts": [ "2021:256:16:41:34.816", - "2021:262:02:37:02.816", - "2021:260:05:36:30.816" + "2021:260:05:33:15.513" ], "datestops": [ "2021:256:17:14:22.816", - "2021:262:02:53:26.816", "2021:260:05:58:22.816" ], "duration": [ "1.97", - "0.98", - "1.31" + "1.51" ], "temps": [ "-111.97", - "-111.56", - "-109.62" + "-109.55" ], "obsids": [ "23989", - "23845", "23645" + ], + "exposure": [ + "5.10", + "25.50" + ], + "limit": [ + "-112.00", + "-110.00" ] } \ No newline at end of file diff --git a/acis_thermal_check/tests/acisfp/test_acisfp_acis_obsids.py b/acis_thermal_check/tests/acisfp/test_acisfp_acis_obsids.py new file mode 100644 index 0000000..66ddc7b --- /dev/null +++ b/acis_thermal_check/tests/acisfp/test_acisfp_acis_obsids.py @@ -0,0 +1,20 @@ +from acis_thermal_check.apps.acisfp_check import ACISFPCheck +from acis_thermal_check.tests.regression_testing import RegressionTester, tests_path + + +def test_FEB2122_acis_obsids(answer_store, test_root): + answer_data = tests_path / "acisfp/answers/FEB2122A_acis_obsids.json" + fp_rt = RegressionTester(ACISFPCheck, test_root=test_root, sub_dir="acis_obsids") + fp_rt.check_acis_obsids("FEB2122A", answer_data, answer_store=answer_store) + + +def test_FEB2822_acis_obsids(answer_store, test_root): + answer_data = tests_path / "acisfp/answers/FEB2822A_acis_obsids.json" + fp_rt = RegressionTester(ACISFPCheck, test_root=test_root, sub_dir="acis_obsids") + fp_rt.check_acis_obsids("FEB2822A", answer_data, answer_store=answer_store) + + +def test_MAR0722_acis_obsids(answer_store, test_root): + answer_data = tests_path / "acisfp/answers/MAR0722A_acis_obsids.json" + fp_rt = RegressionTester(ACISFPCheck, test_root=test_root, sub_dir="acis_obsids") + fp_rt.check_acis_obsids("MAR0722A", answer_data, answer_store=answer_store) diff --git a/acis_thermal_check/tests/acisfp/test_acisfp_hot_acis.py b/acis_thermal_check/tests/acisfp/test_acisfp_hot_acis.py deleted file mode 100644 index bb2fbdd..0000000 --- a/acis_thermal_check/tests/acisfp/test_acisfp_hot_acis.py +++ /dev/null @@ -1,20 +0,0 @@ -from acis_thermal_check.apps.acisfp_check import ACISFPCheck -from acis_thermal_check.tests.regression_testing import RegressionTester, tests_path - - -def test_FEB2122_hot_acis(answer_store, test_root): - answer_data = tests_path / "acisfp/answers/FEB2122A_hot_acis.json" - fp_rt = RegressionTester(ACISFPCheck, test_root=test_root, sub_dir="hot_acis") - fp_rt.check_hot_acis_reporting("FEB2122A", answer_data, answer_store=answer_store) - - -def test_FEB2822_hot_acis(answer_store, test_root): - answer_data = tests_path / "acisfp/answers/FEB2822A_hot_acis.json" - fp_rt = RegressionTester(ACISFPCheck, test_root=test_root, sub_dir="hot_acis") - fp_rt.check_hot_acis_reporting("FEB2822A", answer_data, answer_store=answer_store) - - -def test_MAR0722_hot_acis(answer_store, test_root): - answer_data = tests_path / "acisfp/answers/MAR0722A_hot_acis.json" - fp_rt = RegressionTester(ACISFPCheck, test_root=test_root, sub_dir="hot_acis") - fp_rt.check_hot_acis_reporting("MAR0722A", answer_data, answer_store=answer_store) diff --git a/acis_thermal_check/tests/bep_pcb/answers/JUL2919A_viol.json b/acis_thermal_check/tests/bep_pcb/answers/JUL2919A_viol.json index c314dc9..ca47cf2 100644 --- a/acis_thermal_check/tests/bep_pcb/answers/JUL2919A_viol.json +++ b/acis_thermal_check/tests/bep_pcb/answers/JUL2919A_viol.json @@ -1,10 +1,10 @@ { "run_start": "2019:203:10:57:34.816", "limits": { - "yellow_hi": 33.0, - "planning_hi": 31.0, - "yellow_lo": 18.0, - "planning_lo": 16.0 + "safety.caution.high": 33.0, + "planning.warning.high": 31.0, + "safety.caution.low": 18.0, + "planning.warning.low": 16.0 }, "datestarts": [ "2019:210:01:16:43.016", diff --git a/acis_thermal_check/tests/cea/answers/SEP1922A_viol.json b/acis_thermal_check/tests/cea/answers/SEP1922A_viol.json index ec8a796..49ec830 100644 --- a/acis_thermal_check/tests/cea/answers/SEP1922A_viol.json +++ b/acis_thermal_check/tests/cea/answers/SEP1922A_viol.json @@ -10,8 +10,8 @@ ], "run_start": "2022:261:23:34:46.816", "limits": { - "yellow_hi": 11.0, - "planning_hi": 7.5 + "odb.caution.high": 11.0, + "planning.warning.high": 7.5 }, "duration": [ "0.66" diff --git a/acis_thermal_check/tests/dea/answers/DEC0919A_viol.json b/acis_thermal_check/tests/dea/answers/DEC0919A_viol.json index 46c7e6c..cec873b 100644 --- a/acis_thermal_check/tests/dea/answers/DEC0919A_viol.json +++ b/acis_thermal_check/tests/dea/answers/DEC0919A_viol.json @@ -1,13 +1,13 @@ { "datestarts": [ "2019:343:04:21:33.516", - "2019:347:15:21:34.816", + "2019:347:15:20:15.437", "2019:348:21:47:26.816" ], "datestops": [ - "2019:343:04:29:10.816", + "2019:343:04:29:46.763", "2019:347:15:27:02.816", - "2019:348:22:14:46.816" + "2019:348:22:16:07.208" ], "temps": [ "35.33", @@ -16,12 +16,12 @@ ], "run_start": "2019:338:14:25:26.816", "limits": { - "yellow_hi": 37.2, - "planning_hi": 35.2 + "odb.caution.high": 37.2, + "planning.warning.high": 35.2 }, "duration": [ - "0.46", - "0.33", - "1.64" + "0.49", + "0.41", + "1.72" ] } \ No newline at end of file diff --git a/acis_thermal_check/tests/dpa/answers/JUL3018A_viol.json b/acis_thermal_check/tests/dpa/answers/JUL3018A_viol.json index 6c66021..76945a6 100644 --- a/acis_thermal_check/tests/dpa/answers/JUL3018A_viol.json +++ b/acis_thermal_check/tests/dpa/answers/JUL3018A_viol.json @@ -16,8 +16,8 @@ ], "run_start": "2018:205:00:42:38.816", "limits": { - "yellow_hi": 37.2, - "planning_hi": 35.2 + "odb.caution.high": 37.2, + "planning.warning.high": 35.2 }, "duration": [ "3.94", diff --git a/acis_thermal_check/tests/fep1_actel/answers/JUL2919A_viol.json b/acis_thermal_check/tests/fep1_actel/answers/JUL2919A_viol.json index 91330c4..114e92c 100644 --- a/acis_thermal_check/tests/fep1_actel/answers/JUL2919A_viol.json +++ b/acis_thermal_check/tests/fep1_actel/answers/JUL2919A_viol.json @@ -1,30 +1,30 @@ { "run_start": "2019:203:10:57:34.816", "limits": { - "yellow_hi": 41.0, - "planning_hi": 39.0, - "yellow_lo": 18.0, - "planning_lo": 16.0 + "safety.caution.high": 41.0, + "planning.warning.high": 39.0, + "safety.caution.low": 18.0, + "planning.warning.low": 16.0 }, "datestarts": [ "2019:211:07:39:02.816", "2019:212:06:31:10.816", "2019:215:04:13:10.816", "2019:215:07:51:50.816", - "2019:210:16:09:42.816", - "2019:211:10:55:50.816", + "2019:210:16:07:20.920", + "2019:211:10:52:01.370", "2019:213:00:49:58.816", "2019:214:14:22:14.816", "2019:215:23:43:02.816" ], "datestops": [ "2019:211:07:55:26.816", - "2019:212:07:03:58.816", - "2019:215:05:35:10.816", - "2019:215:10:41:18.816", - "2019:210:16:37:02.816", + "2019:212:07:06:59.629", + "2019:215:05:36:38.007", + "2019:215:10:42:48.498", + "2019:210:16:39:38.371", "2019:211:12:01:26.816", - "2019:213:22:36:30.816", + "2019:213:22:37:01.639", "2019:214:22:01:26.816", "2019:216:05:27:26.816" ], @@ -41,12 +41,12 @@ ], "duration": [ "0.98", - "1.97", - "4.92", - "10.17", - "1.64", - "3.94", - "78.39", + "2.15", + "5.01", + "10.26", + "1.94", + "4.17", + "78.42", "27.55", "20.66" ] diff --git a/acis_thermal_check/tests/fep1_mong/answers/JUL2919A_viol.json b/acis_thermal_check/tests/fep1_mong/answers/JUL2919A_viol.json index b0a03e9..a200630 100644 --- a/acis_thermal_check/tests/fep1_mong/answers/JUL2919A_viol.json +++ b/acis_thermal_check/tests/fep1_mong/answers/JUL2919A_viol.json @@ -1,10 +1,10 @@ { "run_start": "2019:203:10:57:34.816", "limits": { - "yellow_hi": 41.0, - "planning_hi": 39.0, - "yellow_lo": 18.0, - "planning_lo": 16.0 + "safety.caution.high": 41.0, + "planning.warning.high": 39.0, + "safety.caution.low": 18.0, + "planning.warning.low": 16.0 }, "datestarts": [ "2019:210:08:13:01.216", @@ -17,8 +17,8 @@ ], "datestops": [ "2019:210:10:55:55.616", - "2019:211:08:06:22.816", - "2019:212:07:11:38.016", + "2019:211:08:06:38.208", + "2019:212:07:12:09.966", "2019:215:05:27:31.616", "2019:215:10:58:48.416", "2019:214:21:50:30.816", @@ -35,8 +35,8 @@ ], "duration": [ "9.77", - "8.07", - "4.66", + "8.08", + "4.69", "1.05", "11.09", "22.96", diff --git a/acis_thermal_check/tests/psmc/answers/FEB1020A_viol.json b/acis_thermal_check/tests/psmc/answers/FEB1020A_viol.json index 4540dad..31e81f5 100644 --- a/acis_thermal_check/tests/psmc/answers/FEB1020A_viol.json +++ b/acis_thermal_check/tests/psmc/answers/FEB1020A_viol.json @@ -13,8 +13,8 @@ ], "run_start": "2020:041:00:20:02.553", "limits": { - "yellow_hi": 51.5, - "planning_hi": 47.0 + "odb.caution.high": 51.5, + "planning.warning.high": 47.0 }, "duration": [ "10.17", diff --git a/acis_thermal_check/tests/regression_testing.py b/acis_thermal_check/tests/regression_testing.py index 771ee6c..b7c1ea8 100644 --- a/acis_thermal_check/tests/regression_testing.py +++ b/acis_thermal_check/tests/regression_testing.py @@ -4,6 +4,7 @@ import tempfile from pathlib import Path, PurePath +import numpy as np from numpy.testing import assert_allclose, assert_array_equal months = [ @@ -488,6 +489,7 @@ def check_violation_reporting( """ import json + tidx = 5 if self.msid == "fptemp" else 3 with open(viol_json) as f: viol_data = json.load(f) if answer_store: @@ -496,6 +498,8 @@ def check_violation_reporting( viol_data["duration"] = [] viol_data["temps"] = [] if self.msid == "fptemp": + viol_data["exposure"] = [] + viol_data["limit"] = [] viol_data["obsids"] = [] load_year = "20%s" % load_week[-3:-1] next_year = f"{int(load_year)+1}" @@ -518,12 +522,18 @@ def check_violation_reporting( viol_data["datestarts"].append(words[0]) viol_data["datestops"].append(words[1]) viol_data["duration"].append(words[2]) - viol_data["temps"].append(words[3]) + viol_data["temps"].append(words[tidx]) if self.msid == "fptemp": if len(words) > 4: - obsid = words[4] + exposure = words[4] + limit = words[7] + obsid = words[8] else: + exposure = "" + limit = "" obsid = "" + viol_data["exposure"].append(exposure) + viol_data["limit"].append(limit) viol_data["obsids"].append(obsid) else: try: @@ -533,6 +543,8 @@ def check_violation_reporting( assert viol_data["temps"][i] in line if self.msid == "fptemp": assert viol_data["obsids"][i] in line + assert viol_data["exposure"][i] in line + assert viol_data["limit"][i] in line except AssertionError: raise AssertionError( "Comparison failed. Check file at %s." % index_rst, @@ -542,28 +554,35 @@ def check_violation_reporting( with open(viol_json, "w") as f: json.dump(viol_data, f, indent=4) - def check_hot_acis_reporting(self, load_week, hot_json, answer_store=False): + def check_acis_obsids(self, load_week, obsid_json, answer_store=False): import json self.run_model(load_week) out_dir = self.outdir / load_week / self.name - index_rst = out_dir / "index.rst" - hot_data = [] + index_rst = out_dir / "obsid_table.rst" + obsid_data = [] with open(index_rst) as myfile: - read_hot_data = False + read_obsid_data = False for line in myfile.readlines(): words = line.strip().split() if line.startswith("Obsid"): - read_hot_data = True - elif read_hot_data: - if len(words) == 6 and not line.startswith("=="): - hot_data.append(words) + read_obsid_data = True + elif read_obsid_data: + if len(words) == 7 and not line.startswith("=="): + obsid_data.append(words) elif len(words) == 0: - read_hot_data = False + read_obsid_data = False if answer_store: - with open(hot_json, "w") as f: - json.dump(hot_data, f, indent=4) + with open(obsid_json, "w") as f: + json.dump(obsid_data, f, indent=4) else: - with open(hot_json) as f: - hot_data_stored = json.load(f) - assert_array_equal(hot_data, hot_data_stored) + with open(obsid_json) as f: + obsid_data_stored = json.load(f) + try: + assert_array_equal(obsid_data, obsid_data_stored) + except AssertionError: + outlines = "Some entries did not match:\n" + for o1, o2 in zip(obsid_data, obsid_data_stored): + if not np.all(o1 == o2): + outlines += f"{o1} != {o2}\n" + raise AssertionError(outlines) diff --git a/acis_thermal_check/utils.py b/acis_thermal_check/utils.py index b9cf911..336ee8a 100644 --- a/acis_thermal_check/utils.py +++ b/acis_thermal_check/utils.py @@ -5,7 +5,6 @@ import numpy as np import ska_numpy from ska_matplotlib import cxctime2plotdate, set_time_ticks -from xija.limits import get_limit_color TASK_DATA = Path(PurePath(__file__).parent / "..").resolve() @@ -255,7 +254,7 @@ def __init__( self.ax2 = ax2 self.filename = None - def add_limit_line(self, limit, label, ls="-"): + def add_limit_line(self, limit, ls="-", lw=2): """ Add a horizontal line for a given limit to the plot. @@ -263,17 +262,24 @@ def add_limit_line(self, limit, label, ls="-"): ---------- limit : ACISLimit object Contains information about the value of the limit - and the color it should be plotted with. - label : string - The label to give the line. + the color it should be plotted with, and its label. ls : string, optional The line style for the limit line. Default: "-" + lw : float, optional + The line width for the limit line. Default: 2.0 """ + label = limit["display_name"] + # If the label already exists, we shouldn't need to repeat it + # in the legend + for line in self.ax.lines: + if label == line.get_label(): + label = None + break self.ax.axhline( - limit.value, + limit["value"], linestyle=ls, - linewidth=2.0, - color=limit.color, + linewidth=lw, + color=limit["color"], label=label, zorder=2.0, ) @@ -284,6 +290,11 @@ class PredictPlot(PlotDate): _color2 = thermal_blue +class ValidatePlot(PlotDate): + _color = thermal_red + _color2 = thermal_blue + + def get_options(opts=None, use_acis_opts=True): """ Construct the argument parser for command-line options for running @@ -409,7 +420,7 @@ def make_state_builder(name, args, hrc_states=False): args : ArgumentParser arguments The arguments to pass to the StateBuilder subclass. hrc_states : boolean, optional - Whether or not to add HRC-specific states. Default: False + Whether to add HRC-specific states. Default: False """ # Import the dictionary of possible state builders. This # dictionary is located in state_builder.py @@ -467,52 +478,3 @@ def paint_perigee(perigee_passages, plots): for time in perigee_passages[key]: xpos = cxctime2plotdate([time])[0] plot.ax.axvline(xpos, linestyle=":", color=color, linewidth=2.0) - - -class ChandraLimit: - def __init__(self, value, color): - self.value = value - self.color = color - - -def get_acis_limits(msid, model_spec, limits_map=None): - """ - Given a MSID and a model specification (JSON or dict), - return the values and line colors of the limits specified - in the file. - - Parameters - ---------- - msid : string - The MSID to get the limits for. - model_spec : string or dict - The xija model specification. If a string, it is - assumed to be a JSON file to be read in - limits_map : dict, optional - If supplied, this will change the keys of the output - dict, which are normally the limit names in the model - specification, with other names, e.g. replaces - "odb.caution.high" with "yellow_hi". Default: None - - Returns - ------- - A dict of dicts, with each dict corresponding to the value of the - limit and the color of the line on plots. - """ - import json - - if msid == "fptemp_11": - msid = "fptemp" - if limits_map is None: - limits_map = {} - if not isinstance(model_spec, dict): - with open(model_spec) as f: - model_spec = json.load(f) - json_limits = model_spec["limits"][msid] - limits = {} - for k, v in json_limits.items(): - if k == "unit": - continue - key = limits_map.get(k, k) - limits[key] = ChandraLimit(v, get_limit_color(k)) - return limits