diff --git a/kadi/commands/states.py b/kadi/commands/states.py index cc6fca5a..b33704dd 100644 --- a/kadi/commands/states.py +++ b/kadi/commands/states.py @@ -41,6 +41,9 @@ + ["auto_npnt", "pcad_mode", "pitch", "off_nom_roll"] ) +# State keys for SPM-related transitions. +SPM_STATE_KEYS = ["sun_pos_mon", "battery_connect", "eclipse_enable_spm"] + # Default state keys (mostly matches classic command states list) DEFAULT_STATE_KEYS = ( "ccd_count", @@ -711,7 +714,7 @@ class SPMEnableTransition(FixedTransition): command_attributes = {"tlmsid": "AOFUNCEN"} command_params = {"aopcadse": 30} - state_keys = ["sun_pos_mon"] + state_keys = SPM_STATE_KEYS transition_key = "sun_pos_mon" transition_val = "ENAB" @@ -721,7 +724,7 @@ class SPMDisableTransition(FixedTransition): command_attributes = {"tlmsid": "AOFUNCDS"} command_params = {"aopcadsd": 30} - state_keys = ["sun_pos_mon"] + state_keys = SPM_STATE_KEYS transition_key = "sun_pos_mon" transition_val = "DISA" @@ -736,11 +739,51 @@ class SPMEclipseEnableTransition(BaseTransition): Eclipse exit is event type ORBPOINT with TYPE=PEXIT or TYPE=LSPEXIT """ - # Command attributes and params are just for docstring, but actual transition - # command filtering in set_transitions is more complicated. command_attributes = {"type": "ORBPOINT"} command_params = {"event_type": ["PEXIT", "LSPEXIT"]} - state_keys = ["sun_pos_mon"] + state_keys = SPM_STATE_KEYS + default_value = False + + @classmethod + def set_transitions(cls, transitions_dict, cmds, start, stop): + """ + Set transitions for a Table of commands ``cmds``. + + :param transitions_dict: global dict of transitions (updated in-place) + :param cmds: commands (CmdList) + :param start: start time for states + :param stop: stop time for states + + :returns: None + """ + # Preselect only commands that might have an impact here. + state_cmds = cls.get_state_changing_commands(cmds) + + for cmd in state_cmds: + transitions_dict[cmd["date"]]["sun_pos_mon"] = cls.callback + + @classmethod + def callback(cls, date, transitions, state, idx): + if state["eclipse_enable_spm"]: + transition = { + "date": secs2date(date2secs(date) + 11 * 60), + "sun_pos_mon": "ENAB", + } + add_transition(transitions, idx, transition) + + +class EclipseEnableSPM(BaseTransition): + """Flag to indicate whether SPM will be enabled 11 minutes after eclipse exit. + + This is evaluated at the time of eclipse entry and checks that the most recent + battery connect command (via the ``battery_connect`` state) was within 2:05 minutes + of eclipse entry. + """ + + command_attributes = {"type": "ORBPOINT"} + command_params = {"event_type": ["PENTRY", "LSPENTRY"]} + state_keys = SPM_STATE_KEYS + default_value = False @classmethod def set_transitions(cls, transitions_dict, cmds, start, stop): @@ -755,27 +798,49 @@ def set_transitions(cls, transitions_dict, cmds, start, stop): :returns: None """ # Preselect only commands that might have an impact here. - ok = (cmds["tlmsid"] == "EOESTECN") | (cmds["type"] == "ORBPOINT") - cmds = cmds[ok] + state_cmds = cls.get_state_changing_commands(cmds) + + for cmd in state_cmds: + transitions_dict[cmd["date"]]["eclipse_enable_spm"] = cls.callback + + @classmethod + def callback(cls, date, transitions, state, idx): + """Set flag if SPM will be enabled 11 minutes after eclipse exit. + + ``battery_connect_time`` is the time of the battery connect EOESTECN command, + which must occur prior to this command which is eclipse entry. + """ + battery_connect_time = date2secs(state["battery_connect"]) + eclipse_entry_time = date2secs(date) + enable_spm = eclipse_entry_time - battery_connect_time < 125 + transition = {"date": date, "eclipse_enable_spm": enable_spm} + add_transition(transitions, idx, transition) - connect_time = 0 - connect_flag = False - for cmd in cmds: - if cmd["tlmsid"] == "EOESTECN": - connect_time = DateTime(cmd["date"]).secs +class BatteryConnect(BaseTransition): + """Most recent battery connect time (type=COMMAND_SW and tlmsid=EOESTECN)""" - elif cmd["type"] == "ORBPOINT": - if cmd["event_type"] in ("PENTRY", "LSPENTRY"): - entry_time = DateTime(cmd["date"]).secs - connect_flag = entry_time - connect_time < 125 + command_attributes = {"tlmsid": "EOESTECN"} + state_keys = SPM_STATE_KEYS - elif cmd["event_type"] in ("PEXIT", "LSPEXIT") and connect_flag: - scs33 = ( - DateTime(cmd["date"]) + 11 * 60 / 86400 - ) # 11 minutes in days - transitions_dict[scs33.date]["sun_pos_mon"] = "ENAB" - connect_flag = False + default_value = "1999:001:00:00:00.000" + + @classmethod + def set_transitions(cls, transitions, cmds, start, stop): + """ + Set transitions for a Table of commands ``cmds``. + + :param transitions_dict: global dict of transitions (updated in-place) + :param cmds: commands (CmdList) + :param start: start time for states + :param stop: stop time for states + + :returns: None + """ + state_cmds = cls.get_state_changing_commands(cmds) + + for cmd in state_cmds: + transitions[cmd["date"]]["battery_connect"] = cmd["date"] class SCS84EnableTransition(FixedTransition): @@ -1573,7 +1638,7 @@ def get_states( if "did not find transitions" in str(exc): raise ValueError( f"no continuity found for {start=}. Need to have state " - f'transitions following first command at {cmds[0]["date"]} ' + f"transitions following first command at {cmds[0]['date']} " "so use a later start date." ) else: @@ -1946,7 +2011,7 @@ def get_chandra_states(main_args=None): "--merge-identical", default=False, action="store_true", - help="Merge adjacent states that have identical values " "(default=False)", + help="Merge adjacent states that have identical values (default=False)", ) parser.add_argument("--outfile", help="Output file (default=stdout)") diff --git a/kadi/commands/tests/test_states.py b/kadi/commands/tests/test_states.py index 777e11ca..785ab61e 100644 --- a/kadi/commands/tests/test_states.py +++ b/kadi/commands/tests/test_states.py @@ -10,6 +10,7 @@ from astropy.io import ascii from astropy.table import Table from Chandra.Time import DateTime +from cxotime import CxoTime from Ska.engarchive import fetch from testr.test_helper import has_internet @@ -19,8 +20,8 @@ message="kadi commands v1 is deprecated, use v2 instead", ) -from kadi import commands -from kadi.commands import states +from kadi import commands # noqa: E402 +from kadi.commands import states # noqa: E402 try: fetch.get_time_range("dp_pitch") @@ -1433,7 +1434,7 @@ def test_continuity_with_transitions_SPM(): to a list of transitions that are needed for correct continuity. Part of fix for #125. - Start time is ne minute before auto-re-enable of SPM during eclipse handling + Start time is one minute before auto-re-enable of SPM during eclipse handling This is in the middle of the transitions generated by SPMEnableTransition. The test here is seeing the ENAB transition that is one minute after the continuity start time. This will be used by get_states() to get things @@ -1448,6 +1449,7 @@ def test_continuity_with_transitions_SPM(): "sun_pos_mon": "DISA", } + # fmt: off exp = [ " datestart datestop tstart tstop " "sun_pos_mon trans_keys", @@ -1460,6 +1462,7 @@ def test_continuity_with_transitions_SPM(): "2017:087:08:30:50.891 2017:087:10:20:35.838 607077120.075 " "607083705.022 DISA sun_pos_mon", ] + # fmt: on sts = states.get_states(start, stop, state_keys=["sun_pos_mon"]) assert sts.pformat(max_lines=-1, max_width=-1) == exp @@ -1724,3 +1727,73 @@ def test_nsm_continuity(): """ # Prior to the fix this raised an exception, so just check that it runs. states.get_continuity("2022:301:12:42:00", scenario="flight") + + +def test_sun_pos_mon_within_eclipse(): + """Test for #289 where sun_pos_mon was not being set correctly within eclipse. + + Relevant commands near an eclipse:: + + 2022:109:21:27:27.034 | COMMAND_SW | EOESTECN | APR1822A | + 2022:109:21:29:27.034 | ORBPOINT | None | APR1822A | PENTRY + 2022:109:22:03:07.034 | ORBPOINT | None | APR1822A | PEXIT + + Battery connect time is 2022:109:21:27:27.034 + Expected sun_pos_mon ENAB is at 2022:109:22:14:07.034. + """ + starts = CxoTime( + [ + "2022:109:21:24:00.000", # Early + "2022:109:21:27:27.033", # 1 ms before battery connect + "2022:109:21:27:27.035", # 1 ms after battery connect + "2022:109:21:29:27.033", # 1 ms before pentry + "2022:109:21:29:27.035", # 1 ms after pentry + "2022:109:22:03:07.033", # 1 ms before pexit + "2022:109:22:03:07.035", # 1 ms after pexit + "2022:109:22:14:07.033", # 1 ms before OBC autonomous sun_pos_mon ENAB + ] + ) + + stop = "2022:110:00:00:00.000" + spm_state_keys = states.SPM_STATE_KEYS + + for start in starts: + exp_start_date = max(start.date, "2022:109:21:27:27.034") + # fmt: off + exp = [ + " datestart sun_pos_mon battery_connect eclipse_enable_spm", + "--------------------- ----------- --------------------- ------------------", + f"{exp_start_date } DISA 2022:109:21:27:27.034 True", + "2022:109:22:14:07.034 ENAB 2022:109:21:27:27.034 True", + ] + # fmt: on + + sts = states.get_states( + start, stop, state_keys=spm_state_keys, merge_identical=True + ) + + names = ["datestart"] + spm_state_keys + assert sts[names][-2:].pformat_all() == exp + + +def test_sun_pos_mon_within_eclipse_no_spm_enab(): + """ + Test a case where battery connect is more than 125 sec before pentry. + + 2005:014:15:31:36.410 | COMMAND_SW | EOESTECN | JAN1005B + 2005:014:15:33:49.164 | ORBPOINT | None | JAN1005B | PENTRY + 2005:014:16:38:09.164 | ORBPOINT | None | JAN1005B | PEXIT + """ + sts = states.get_states( + "2005:014:16:38:10", # Just after pexit + "2005:014:17:00:00", # 22 min later + state_keys=states.SPM_STATE_KEYS, + ) + + exp = [ + " datestart sun_pos_mon battery_connect eclipse_enable_spm", + "--------------------- ----------- --------------------- ------------------", + "2005:014:16:38:10.000 DISA 2005:014:15:31:36.410 False", + ] + names = ["datestart"] + states.SPM_STATE_KEYS + assert sts[names].pformat_all() == exp