Commands and states¶
Commands
The Commands archive is a table of every load command that has been run, or is currently approved to be run, on the spacecraft since 2002. This archive accounts for load stoppages, replans, and certain non-load commands like ACIS CTI runs or Normal Sun Mode transitions.
The flight Commands archive v2 relies on the Chandra Command Events sheet and OCCweb FOT mission planning approved load products to maintain the commands database. This archive provides an up-to-date view of commands and states even during anomalies. The Chandra Command Events sheet is kept current during anomalies by FOT MP and helps facilitate team-wide communication of non-load spacecraft commanding.
States and continuity
Coupled with the commands archive is functionality determine Chandra states and continuity:
State values of certain parameters of interest (obsid, SIM-Z position, commanded attitude, ACIS power configuration, etc) over an interval of time during which all parameters have been unaffected by commanding and are unchanged. This provides a compact way to represent the impact of commanding on the spacecraft state over time and is used in xija thermal model predictions.
Continuity values of certain parameters of interest at a particular time. The continuity represents the state values at a moment in time and also includes the date of the last command which affected the state.
The State keys section lists all state parameters which are implemented in the installed code. Note that a key design feature is that is it straightforward for users to implement their own states, often with just a few lines of code. See the User-defined states section for details.
Commands archive v2¶
For more details of the commands v2 archive, including important information about network access, the timeliness of commands, configuration and process details, please see:
Initial setup¶
In order to use commands archive v2 to always get the most up-to-date commands,
you need to set up automated access to OCCweb so the code can fetch recent
command loads. This is done by creating a file at the top level of your home
directory called .netrc
which includes the following contents:
machine occweb
login <OCCweb username>
password <OCCweb password>
If you have other authentication entries in the same file (e.g. for lucky
)
then there needs to be a blank line between entries.
Important
Make sure the file is readable only by you. On linux this is done with
chmod og-rwx ~/.netrc
.
Getting commands¶
The basic way to select commands is with the get_cmds()
method. For example you can find
load commands from early in 2013 with:
>>> cmds = commands.get_cmds('2013:001:00:00:00', '2013:001:00:56:10')
>>> print(cmds)
date type tlmsid scs step time source vcdu params
--------------------- ---------- ---------- --- ---- ------------- -------- ------- ------
2013:001:00:37:37.653 ORBPOINT None 0 0 473387924.837 DEC2412B 5533112 N/A
2013:001:00:53:07.181 COMMAND_SW AOACRSTD 129 1524 473388854.365 DEC2412B 5584176 N/A
2013:001:00:54:07.181 COMMAND_SW AOFUNCDS 129 1526 473388914.365 DEC2412B 5584410 N/A
2013:001:00:55:07.181 COMMAND_SW AOFUNCDS 129 1528 473388974.365 DEC2412B 5584644 N/A
2013:001:00:56:07.181 COMMAND_SW AONMMODE 129 1530 473389034.365 DEC2412B 5584878 N/A
2013:001:00:56:07.181 ACISPKT AA00000000 132 1620 473389034.365 DEC2412B 5584878 N/A
2013:001:00:56:07.181 SIMTRANS None 132 1623 473389034.365 DEC2412B 5584878 N/A
2013:001:00:56:07.438 COMMAND_SW AONM2NPE 129 1532 473389034.622 DEC2412B 5584879 N/A
In the get_cmds()
method, commands are selected with start <= date < stop
, where each
of these are evaluated as a date string with millisec precision. In order to get commands
at exactly a certain date you need to select with the date
argument:
>>> print(commands.get_cmds(date='2013:001:00:56:07.181'))
date type tlmsid scs step time source vcdu params
--------------------- ---------- ---------- --- ---- ------------- -------- ------- ------
2013:001:00:56:07.181 COMMAND_SW AONMMODE 129 1530 473389034.365 DEC2412B 5584878 N/A
2013:001:00:56:07.181 ACISPKT AA00000000 132 1620 473389034.365 DEC2412B 5584878 N/A
2013:001:00:56:07.181 SIMTRANS None 132 1623 473389034.365 DEC2412B 5584878 N/A
The output cmds
is based on the astropy Table object with many powerful and handy
features built in. For instance you could sort by type
, tlmsid
and date
:
>>> cmds_type = cmds.copy()
>>> cmds_type.sort(['type', 'tlmsid', 'date'])
>>> print(cmds_type)
date type tlmsid scs step time source vcdu params
--------------------- ---------- ---------- --- ---- ------------- -------- ------- ------
2013:001:00:56:07.181 ACISPKT AA00000000 132 1620 473389034.365 DEC2412B 5584878 N/A
2013:001:00:53:07.181 COMMAND_SW AOACRSTD 129 1524 473388854.365 DEC2412B 5584176 N/A
2013:001:00:54:07.181 COMMAND_SW AOFUNCDS 129 1526 473388914.365 DEC2412B 5584410 N/A
2013:001:00:55:07.181 COMMAND_SW AOFUNCDS 129 1528 473388974.365 DEC2412B 5584644 N/A
2013:001:00:56:07.438 COMMAND_SW AONM2NPE 129 1532 473389034.622 DEC2412B 5584879 N/A
2013:001:00:56:07.181 COMMAND_SW AONMMODE 129 1530 473389034.365 DEC2412B 5584878 N/A
2013:001:00:37:37.653 ORBPOINT None 0 0 473387924.837 DEC2412B 5533112 N/A
2013:001:00:56:07.181 SIMTRANS None 132 1623 473389034.365 DEC2412B 5584878 N/A
You can print a single command and get all the information about it:
>>> print(cmds[5])
2013:001:00:56:07.181 ACISPKT tlmsid=AA00000000 scs=132 step=1620 source=DEC2412B vcdu=5584878 cmds=3 packet(40)=D80000300030603001300 words=3
This command has a number of attributes like date
or tlmsid
(shown in the original table) as well as command parameters: cmds
, packet(40)
, and words
. You can access any of the attributes or parameters like a dictionary:
>>> print(cmds[5]['packet(40)'])
D80000300030603001300
You probably noticed the first time we printed cmds
that the command parameters
params
were all listed as N/A
(Not Available). What happens if we print the
table again:
date type tlmsid scs step time source vcdu params
--------------------- ---------- ---------- --- ---- ------------- -------- ------- --------------------------------------------
2013:001:00:37:37.653 ORBPOINT None 0 0 473387924.837 DEC2412B 5533112 N/A
2013:001:00:53:07.181 COMMAND_SW AOACRSTD 129 1524 473388854.365 DEC2412B 5584176 N/A
2013:001:00:54:07.181 COMMAND_SW AOFUNCDS 129 1526 473388914.365 DEC2412B 5584410 N/A
2013:001:00:55:07.181 COMMAND_SW AOFUNCDS 129 1528 473388974.365 DEC2412B 5584644 N/A
2013:001:00:56:07.181 COMMAND_SW AONMMODE 129 1530 473389034.365 DEC2412B 5584878 N/A
2013:001:00:56:07.181 ACISPKT AA00000000 132 1620 473389034.365 DEC2412B 5584878 cmds=3 packet(40)=D80000300030603001300 ...
2013:001:00:56:07.181 SIMTRANS None 132 1623 473389034.365 DEC2412B 5584878 N/A
2013:001:00:56:07.438 COMMAND_SW AONM2NPE 129 1532 473389034.622 DEC2412B 5584879 N/A
So what happened? The answer is that for performance reasons the CommandTable
class is
lazy about loading the command parameters, and only does so when you directly request the
parameter value (as we did with packet(40)
). If you want to just fetch them all
at once you can do so with the fetch_params()
method:
>>> cmds.fetch_params()
>>> print(cmds)
date type tlmsid scs step time source vcdu params
--------------------- ---------- ---------- --- ---- ------------- -------- ------- --------------------------------------------
2013:001:00:37:37.653 ORBPOINT None 0 0 473387924.837 DEC2412B 5533112 event_type=EQF013M
2013:001:00:53:07.181 COMMAND_SW AOACRSTD 129 1524 473388854.365 DEC2412B 5584176 hex=8032000 msid=AOACRSTD
2013:001:00:54:07.181 COMMAND_SW AOFUNCDS 129 1526 473388914.365 DEC2412B 5584410 aopcadsd=21 hex=8030215 msid=AOFUNCDS
2013:001:00:55:07.181 COMMAND_SW AOFUNCDS 129 1528 473388974.365 DEC2412B 5584644 aopcadsd=32 hex=8030220 msid=AOFUNCDS
2013:001:00:56:07.181 COMMAND_SW AONMMODE 129 1530 473389034.365 DEC2412B 5584878 hex=8030402 msid=AONMMODE
2013:001:00:56:07.181 ACISPKT AA00000000 132 1620 473389034.365 DEC2412B 5584878 cmds=3 packet(40)=D80000300030603001300 ...
2013:001:00:56:07.181 SIMTRANS None 132 1623 473389034.365 DEC2412B 5584878 pos=-99616
2013:001:00:56:07.438 COMMAND_SW AONM2NPE 129 1532 473389034.622 DEC2412B 5584879 hex=8030601 msid=AONM2NPE
Finally, note that you can request the value of an attribute or parameter for the entire
command table. Note that command rows without that parameter will have a None
object:
>>> print(cmds['msid'])
msid
--------
None
AOACRSTD
AOFUNCDS
AOFUNCDS
AONMMODE
None
None
AONM2NPE
Getting observations¶
The commands archive includes special LOAD_EVENT
commands (like the RLTT) that
contain information about observations in the loads. An “observation” is defined
as a dwell in normal point mode following a maneuver and includes most OR’s and
ER’s. These are most easily accessed via the
get_observations()
function. For example:
>>> from kadi.commands import get_observations
>>> obss = get_observations(obsid=26330)
>>> obss[0]
{'obsid': 26330,
'simpos': 73296,
'obs_stop': '2022:075:19:10:35.734',
'manvr_start': '2022:075:17:39:54.696',
'targ_att': (0.105773397, -0.727314387, -0.579818109, 0.351620152),
'npnt_enab': True,
'obs_start': '2022:075:17:57:55.985',
'prev_att': (0.113370245, -0.848288771, -0.329080853, 0.399072852),
'starcat_idx': 211630,
'source': 'MAR1422A'}
Notice that the command always returns a list of observations, even for a query asking for a specific ObsID. The reason is that cases of multiple observations with the same ObsID are relatively common, in particular after SCS-107 stops the observing loads this will happen. The commands archive reflects the commands that ran on-board, and since ObsID updates are in the observing loads those commands no longer run after SCS-107.
For example, ObsID 65526 was manually commanded after the HRC B-side anomaly and persisted for 64 distinct observations in the vehicle loads:
>>> obss = get_observations(obsid=65526) # ObsID after HRC B-side anomaly
>>> len(obss)
64
Getting all the observations covering years or more is reasonably fast, typically seconds the first query and then << 1 sec for subsequent queries.
>>> %time obss = get_observations(start='2020:001', stop='2022:001')
CPU times: user 28.4 ms, sys: 952 µs, total: 29.4 ms
Wall time: 28.7 ms
For a large number of observations like this you may find it convenient to turn
this into an astropy Table
:
>>> from astropy.table import Table
>>> obss = Table(obss)
>>> obss
<Table length=4018>
obsid simpos obs_stop manvr_start ... obs_start prev_att [4] starcat_idx source
int64 int64 str21 str21 ... str21 float64 int64 str8
----- ------ --------------------- --------------------- ... --------------------- ---------------------------- ----------- --------
47575 75624 2020:001:06:51:13.985 2020:001:06:15:56.053 ... 2020:001:06:46:33.236 -0.51363412 .. 0.832139148 166529 DEC2319A
23000 75624 2020:001:19:18:15.914 2020:001:06:51:24.236 ... 2020:001:07:17:16.164 -0.475120775 .. 0.693367999 166530 DEC2319A
47574 75624 2020:001:19:48:58.024 2020:001:19:18:26.165 ... 2020:001:19:44:17.276 -0.221358674 .. 0.710485779 166531 DEC2319A
... ... ... ... ... ... ... ... ...
25803 75624 2021:365:13:38:31.497 2021:365:05:20:58.062 ... 2021:365:05:47:31.748 -0.488490169 .. 0.0261316575 170515 DEC3021A
26264 75624 2021:365:18:39:19.983 2021:365:13:38:41.748 ... 2021:365:14:08:20.234 -0.0658948803 .. 0.350032226 170516 DEC3021A
26247 75624 2022:001:05:21:28.604 2021:365:18:39:30.234 ... 2021:365:19:06:05.650 0.553670113 .. 0.0635036584 170517 DEC3021A
Under the hood¶
The observation information is stored as LOAD_EVENT
commands that can be viewed
directly:
>>> from kadi.commands import get_cmds
>>> cmds = get_cmds('2022:001', '2022:002', type='LOAD_EVENT')
>>> print(cmds)
date type tlmsid scs step time source vcdu params
--------------------- ---------- ------ --- ---- ------------- -------- ---- ------
2022:001:05:48:44.808 LOAD_EVENT OBS 0 0 757403393.992 DEC3021A -1 N/A
2022:001:09:42:05.439 LOAD_EVENT OBS 0 0 757417394.623 DEC3021A -1 N/A
2022:001:11:37:20.405 LOAD_EVENT OBS 0 0 757424309.589 DEC3021A -1 N/A
2022:001:15:03:39.654 LOAD_EVENT OBS 0 0 757436688.838 DEC3021A -1 N/A
2022:001:15:29:26.255 LOAD_EVENT OBS 0 0 757438235.439 DEC3021A -1 N/A
2022:001:17:33:53.255 LOAD_EVENT OBS 0 0 757445702.439 DEC3021A -1 N/A
>>> cmds[0]['params']
{'obsid': 45814,
'simpos': -99616,
'obs_stop': '2022:001:09:13:04.557',
'manvr_start': '2022:001:05:21:38.855',
'targ_att': (0.530730117, 0.556620885, 0.610704042, 0.188518716),
'npnt_enab': True,
'obs_start': '2022:001:05:48:44.808',
'prev_att': (-0.0743435142, -0.559183412, -0.804323901, 0.186681591),
'starcat_idx': 170518}
As with get_cmds()
in the v2 archive, you can provide a
scenario
keyword to get_observations()
to
select a custom or 'flight'
scenario.
Getting star catalogs¶
The ACA star catalogs associated with observations can be retrieved using the
get_starcats()
function. For example:
>>> from kadi.commands import get_starcats
>>> acas = get_starcats(obsid=26330)
>>> acas[0]
<ACATable length=11>
slot idx id type sz mag maxmag yang zang dim res halfw
int64 int64 int64 str3 str3 float64 float64 float64 float64 int64 int64 int64
----- ----- --------- ---- ---- ------- ------- -------- -------- ----- ----- -----
0 1 2 FID 8x8 7.00 8.00 -773.14 -1862.22 1 1 25
1 2 4 FID 8x8 7.00 8.00 2140.38 46.50 1 1 25
2 3 5 FID 8x8 7.00 8.00 -1826.24 40.03 1 1 25
3 4 194257752 BOT 6x6 6.09 7.59 -2389.55 -1716.62 28 1 160
4 5 264114816 BOT 6x6 8.64 10.19 -2.30 -2430.32 28 1 160
5 6 194249696 BOT 6x6 8.76 10.27 -2129.92 -2447.14 28 1 160
6 7 263198168 BOT 6x6 8.81 10.33 -375.16 2416.16 28 1 160
0 8 263199776 ACQ 6x6 10.13 11.20 1385.78 1949.73 8 1 60
1 9 264113448 ACQ 6x6 10.27 11.20 -187.29 -1336.05 8 1 60
2 10 263201064 ACQ 6x6 10.44 11.20 529.26 324.30 8 1 60
7 11 263196576 ACQ 6x6 10.56 11.20 -1046.29 258.97 16 1 100
Note
The ACATable
objects that are returned can be plotted but they
are not fully equivalent to the catalogs that proseco
would return. The
CCD temperatures are set to -20 C and the .acqs
and .guides
attributes
are stubbed with empty tables.
Getting dicts instead of ACA tables¶
Another performance option which can be useful in some cases is to set the
as_dict
keyword to True
. This will return a list of dictionaries instead
of converting each catalog into an ACATable
object.
Getting a Table of catalog entries¶
For some use cases you want a single table of all star catalog entries matching
the specified criteria. This can be done with the
get_starcats_as_table()
function. This is
roughly the equivalent of doing a Table vstack
of the individual
ACATable
catalogs but is much faster. In addition two columns obsid
and
starcat_date
are added to provide this information for each entry.
Caching¶
In order to significantly speed up the retrieval of historical star catalogs for
typical ACA operations analysis, the results of each call to get_starcats()
are (by default) cached in a file ~/.kadi/starcats.db
.
This caching is controlled by a configuration parameter cache_starcats
. To
permanently disable caching you can edit your configuration file (see
Configuration options). To disable caching for a single call to
get_starcats()
, you can do something like:
>>> from kadi.commands import get_starcats, conf
>>> with conf.set_temp('cache_starcats', False):
... starcats = get_starcats('2022:001', '2022:002')
Chandra states and continuity¶
To get started, import the kadi.commands.states
module:
>>> from kadi.commands import states
The fundamental idea of the states
module is that one has a state quantity
such as obsid
or si_mode
which is impacted by commands that Chandra
runs. By stepping through all commands and maintaining a state vector during
that process, one assembles the state history relevant to those commands.
The identifer for each of these quantities is referred to as a state_key
States¶
A commanded state is an interval of time over which certain parameters of interest (obsid, SIM-Z position, commanded attitude, ACIS power configuration, etc) are unchanged.
The get_states()
function is the workhorse for dynamic commanded states. This
function is fairly flexible and is roughly equivalent to the combination of the legacy
Chandra.cmd_states
functions get_states()
and fetch_states()
.
States over date range¶
To get the commanded states over a date range you can do the following, which internally
does a call to get_cmds()
in order to get commands over the start
/ stop
date
range:
>>> states.get_states('2017:001:21:00:00', '2017:002:11:29:00',
... state_keys=['obsid', 'simpos', 'clocking'])
<Table length=9>
datestart datestop obsid simpos clocking trans_keys
str21 str21 int64 int64 int64 object
--------------------- --------------------- ----- ------ -------- ---------------
2017:001:21:00:00.000 2017:001:21:02:06.467 18140 75624 1
2017:001:21:02:06.467 2017:001:21:05:06.467 18140 75624 0 clocking
2017:001:21:05:06.467 2017:001:21:05:10.467 19973 75624 0 clocking,obsid
2017:001:21:05:10.467 2017:001:21:05:14.467 19973 75624 0 clocking
2017:001:21:05:14.467 2017:001:21:05:38.467 19973 75624 0 clocking
2017:001:21:05:38.467 2017:001:21:06:45.467 19973 75624 0 clocking
2017:001:21:06:45.467 2017:002:11:23:43.185 19973 75624 1 clocking
2017:002:11:23:43.185 2017:002:11:26:43.185 19973 -99616 0 clocking,simpos
2017:002:11:26:43.185 2017:002:11:29:00.000 50432 -99616 0 obsid
Each state has a start and a stop date, the values for the requested state keys, and a
column called trans_keys
that specifies which keys had their values updated to start
this state.
The first thing to note is that datestop
for a state is always the same as the
datestart
for the following state. There is no gap, and strictly speaking the state
values apply for the date range datestart <= date < datestop
. This is the same as for
getting commands. Next note that the first datestart
and final datestop
match
exactly the input start
and stop
for the function call. This reflects that we
only “know” the states over the time range for which commands were requested.
The astute reader will notice that the 3rd through 6th row says clocking
was
updated, but looking at values they are all 0
. What’s going on? The answer is that,
by default, get_states()
breaks the state if the value was commanded, regardless of
whether the value actually changed. So let’s dig in to the commands at exactly the state
transition time of the 3rd row:
>>> print(commands.get_cmds(date='2017:001:21:05:06.467'))
date type tlmsid scs step timeline_id params
--------------------- -------- ---------- --- ---- ----------- ------
2017:001:21:05:06.467 MP_OBSID COAOSQID 131 400 426102266 N/A
2017:001:21:05:06.467 ACISPKT AA00000000 131 403 426102266 N/A
So there was an ACIS stop science, which sets clocking to 0
even though it
was already 0
(from the previous stop science 3 minutes earlier). If you are
getting states for thermal model computation then you don’t care about these identical
states. In this case specify merge_identical=True
in the function call:
>>> sts = states.get_states('2017:001:21:00:00', '2017:002:11:29:00',
... state_keys=['obsid', 'simpos', 'clocking'],
... merge_identical=True)
>>> sts
<Table length=6>
datestart datestop obsid simpos clocking trans_keys
str21 str21 int64 int64 int64 object
--------------------- --------------------- ----- ------ -------- ---------------
2017:001:21:00:00.000 2017:001:21:02:06.467 18140 75624 1
2017:001:21:02:06.467 2017:001:21:05:06.467 18140 75624 0 clocking
2017:001:21:05:06.467 2017:001:21:06:45.467 19973 75624 0 obsid
2017:001:21:06:45.467 2017:002:11:23:43.185 19973 75624 1 clocking
2017:002:11:23:43.185 2017:002:11:26:43.185 19973 -99616 0 clocking,simpos
2017:002:11:26:43.185 2017:002:11:29:00.000 50432 -99616 0 obsid
As a side note, although the trans_keys
column looks like a string, that is
a bit of trickery that happens when you print the states table. In fact each row
entry is a Python set()
object. In order to see when obsid
changed in the
above query you could do:
>>> ['obsid' in row['trans_keys'] for row in sts]
[False, False, True, False, False, True]
Command line interface¶
One can do the same thing as above from the command-line using the get_chandra_states
command. This outputs the table (sans trans_keys) in a space-delimited format
to the console or a specified file.
$ get_chandra_states --start 2017:001:21:00:00 --stop 2017:002:11:29:00 \
--state-keys=obsid,simpos,clocking \
--merge-identical
datestart datestop obsid simpos clocking
2017:001:21:00:00.000 2017:001:21:02:06.467 18140 75624 1
2017:001:21:02:06.467 2017:001:21:05:06.467 18140 75624 0
2017:001:21:05:06.467 2017:001:21:06:45.467 19973 75624 0
2017:001:21:06:45.467 2017:002:11:23:43.185 19973 75624 1
2017:002:11:23:43.185 2017:002:11:26:43.185 19973 -99616 0
2017:002:11:26:43.185 2017:002:11:29:00.000 50432 -99616 0
The available options are:
$ get_chandra_states --help
usage: get_chandra_states [-h] [--start START] [--stop STOP]
[--state-keys STATE_KEYS] [--merge-identical]
[--outfile OUTFILE]
Ouput the Chandra commanded states over a date range as a space-delimited
ASCII table.
optional arguments:
-h, --help show this help message and exit
--start START Start date (default=Now-10 days)
--stop STOP Stop date (default=None)
--state-keys STATE_KEYS
Comma-separated list of state keys
--merge-identical Merge adjacent states that have identical values
(default=False)
--outfile OUTFILE Output file (default=stdout)
Using the command line interface and a single state key, or a related set that change due to a single command, one can replicate the information in a backstop history file. For instance here
$ tail <...>/JAN0818/ofls/History/ATTITUDE.txt
2018006.072916206 | -5.27899874e-01 -6.92042461e-01 -4.90427812e-01 4.33533892e-02
2018006.103533882 | 4.51367966e-01 6.45077701e-01 6.14710906e-01 4.76678196e-02
2018006.130755248 | -4.28324009e-01 -4.40000915e-01 3.57368959e-01 7.03722364e-01
2018006.214420159 | -3.23403971e-01 -6.11564724e-01 -7.15954877e-01 9.38460120e-02
2018007.024414705 | -4.16664564e-01 -6.83613678e-01 -5.86236582e-01 1.24055031e-01
2018007.164807705 | -5.04030078e-01 -7.09485195e-01 -4.78304550e-01 1.17512532e-01
$ get_chandra_states --start 2018:006:07:29:16.206 --stop 2018:007:16:50:00 \
--state-keys=targ_q1,targ_q2,targ_q3,targ_q4
datestart datestop targ_q1 targ_q2 targ_q3 targ_q4
2018:006:07:29:16.206 2018:006:10:35:33.882 -0.527899874 -0.692042461 -0.490427812 0.0433533892
2018:006:10:35:33.882 2018:006:13:07:55.248 0.451367966 0.645077701 0.614710906 0.0476678196
2018:006:13:07:55.248 2018:006:21:44:20.159 -0.428324009 -0.440000915 0.357368959 0.703722364
2018:006:21:44:20.159 2018:007:02:44:14.705 -0.323403971 -0.611564724 -0.715954877 0.093846012
2018:007:02:44:14.705 2018:007:16:48:07.705 -0.416664564 -0.683613678 -0.586236582 0.124055031
2018:007:16:48:07.705 2018:007:16:50:00.000 -0.504030078 -0.709485195 -0.47830455 0.117512532
To see more examples of this look at the backstop history section of the testing file kadi/commands/tests/test_states.py. All of the supported state keys that reproduce backstop history files are tested here.
States from commands¶
Instead of relying on get_states()
to get the commands and continuity, you can do things
manually. For example:
>>> start, stop = ('2017:001:21:00:00', '2017:002:11:29:00')
>>> state_keys=['obsid', 'simpos', 'clocking']
>>> cmds = commands.get_cmds(start, stop)
>>> continuity = states.get_continuity(start, state_keys)
>>> states.get_states(cmds=cmds, continuity=continuity,
... state_keys=state_keys,
... merge_identical=True)
<Table length=5>
datestart datestop obsid simpos clocking trans_keys
str21 str21 int64 int64 int64 object
--------------------- --------------------- ----- ------ -------- ---------------
2017:001:21:02:06.467 2017:001:21:05:06.467 18140 75624 0 clocking
2017:001:21:05:06.467 2017:001:21:06:45.467 19973 75624 0 obsid
2017:001:21:06:45.467 2017:002:11:23:43.185 19973 75624 1 clocking
2017:002:11:23:43.185 2017:002:11:26:43.185 19973 -99616 0 clocking,simpos
2017:002:11:26:43.185 2017:002:11:26:43.185 50432 -99616 0 obsid
In the call to get_states()
, if you omit the continuity
argument it will be determined
internally using the first command date.
This manual process is normally what would be done in a load review code where one needs to consider up to four different elements:
Continuity value from some moment, for instance the time of last available telemetry for thermal model propagation.
Commands from the continuity time until the start of loads.
Non-load commands (e.g. a possible CTI run)
Load commands
In this case the calling code is responsible for logic to assemble a single commands table
for the cmds
argument as a CommandTable
object.
Note
The plan is to provide convenience methods and documentation to make this process more straightforward. E.g.:
# Get commands for new loads
bs_cmds = parse_cm.read_backstop(backstop_file)
load_start = bs_cmds[0]['date']
# Get last telem from Ska archive. NOTE: we can and should allow for use
# of MAUDE here to reduce propagation!
last_tlm_date = fetch.get_time_range('1dpamzt', format='date')[1]
# Get approved commands from available telemetry through start of new loads
cmds = commands.get_cmds(last_tlm_date, load_start)
# Get pseudo-node values by running thermal model between
# last_tlm_date - 3 days to last_tlm_date, using estimate or
# fixed value of pseudo-node.
# Add backstop commands. The ``add_commands`` method will sort, but up to
# user to make sure there is no overlap.
cmds.add_commands(bs_cmds) # not yet implemented
# Optionally insert any non-load commands, e.g. for a CTI that may or may not happen
non_load_cmds = ...
cmds.add_commands(non_load_cmds)
sts = states.get_states(cmds=cmds, state_keys=[...])
Continuity¶
To get the continuity state for a desired set of state keys at a certain time, use
get_continuity()
. Before doing this, recall that in IPython one can always get
help on a function, class, or method with <something>?
or help(<something>)
.
So here is how to get help on the get_continuity()
:
>>> states.get_continuity?
Signature: states.get_continuity(date=None, state_keys=None, lookbacks=(7, 30, 180, 1000))
Docstring:
Get the state and transition dates at ``date`` for ``state_keys``.
This function finds the state at a particular date by fetching commands
prior to that date and determine the states. It returns dictionary
``continuity`` provides the state values. Included in this dict is a special
key ``__dates__`` which provides the corresponding date at which the
state-changing command occurred.
Since some state keys like ``pitch`` change often (many times per day) while
others like ``letg`` may not change for weeks, this function does dynamic
lookbacks from ``date`` to find transitions for each key. By default it
will try looking back 7 days, then 30 days, then 180 days, and finally 1000
days. This lookback sequence can be controlled with the ``lookbacks``
argument.
If ``state_keys`` is ``None`` then the default keys ``states.DEFAULT_STATE_KEYS``
is used. This corresponds to the "classic" Chandra commanded states (obsid,
ACIS, PCAD, and mechanisms).
:param date: date (DateTime compatible, default=NOW)
:param state_keys: list of state keys or str (one state key) or None
:param lookbacks: list of lookback times in days (default=[7, 30, 180, 1000])
:returns: dict of state values
So let’s get the state of obsid
and si_mode
at 2017:300:00:00:00
:
>>> continuity = states.get_continuity('2017:300:00:00:00', ['obsid', 'si_mode'])
>>> continuity
{'__dates__': {'obsid': '2017:299:21:50:34.193',
'si_mode': '2017:299:22:02:41.439'},
'obsid': 19385,
'si_mode': 'TE_00A02'}
The return value is a dict
which has key/value pairs for each of the
desired state keys. It also has a __dates__
item which has the
corresponding date when state key changed value because of a command.
To prove this, let’s look at the commands exactly at the state transition time:
>>> cmds = commands.get_cmds(date=continuity['__dates__']['obsid'])
>>> cmds.fetch_params()
>>> print(cmds)
date type tlmsid scs step timeline_id params
--------------------- -------- -------- --- ---- ----------- ---------------
2017:299:21:50:34.193 MP_OBSID COAOSQID 131 495 426102876 cmds=3 id=19385
If no value is supplied for the state_keys
argument then the default set of
state keys shown below is used:
>>> states.DEFAULT_STATE_KEYS
('ccd_count',
'clocking',
'dec',
'dither',
'fep_count',
'hetg',
'letg',
'obsid',
'off_nom_roll',
'pcad_mode',
'pitch',
'power_cmd',
'q1',
'q2',
'q3',
'q4',
'ra',
'roll',
'si_mode',
'simfa_pos',
'simpos',
'targ_q1',
'targ_q2',
'targ_q3',
'targ_q4',
'vid_board')
State keys¶
The list below shows available state keys along with a list of the transition classes which affect the keys.
acisfp_setpoint
aoephem1
,aoephem2
,aoratio
,aoargper
,aoeccent
,ao1minus
,ao1plus
,aomotion
,aoiterat
,aoorbang
,aoperige
,aoascend
,aosini
,aoslr
,aosqrtmu
clocking
,power_cmd
,vid_board
,fep_count
,si_mode
,ccd_count
dither
dither_phase_pitch
,dither_phase_yaw
,dither_ampl_pitch
,dither_ampl_yaw
,dither_period_pitch
,dither_period_yaw
eclipse
eclipse_timer
ephem_update
format
hrc_15v
hrc_24v
hrc_i
hrc_s
letg
,hetg
,grating
obsid
orbit_point
q1
,q2
,q3
,q4
,targ_q1
,targ_q2
,targ_q3
,targ_q4
,ra
,dec
,roll
,auto_npnt
,pcad_mode
,pitch
,off_nom_roll
radmon
scs84
scs98
simfa_pos
simpos
subformat
sun_pos_mon
Implementation¶
Basic design concepts for transition classes:
All state-key specific information is encapsulated in Transition classes. These inherit from
BaseTransition
and have attributes to specify:Command that generates state change
State key(s) that require this transition
Simple cases are handled using
BaseTransition
sub-classes that need only define class attributes.Transition classes are never instantiated, they contain only class methods.
Transition classes have two key methods:
get_state_changing_commands()
Quickly get a list of applicable commands using (usually) numpy filtering instead of looping and if/elif through every command. This is done in the method and allows for getting a year of states in < 10-20 seconds. This requirement drives some other code complexity, in particular transition function callbacks, where a transition is specified as a function that gets called during state evaluation.
set_transitions()
Given a table of applicable commands, generate corresponding state transitions as a dict of state key updates.
Once all transition dicts have been collected into a time-ordered list they are evaluated in order to accumulate discrete states. Transition callback functions can dynamically add downstream transitions during this process to handle events like a maneuver, where the current state is required to generate mid-maneuver attitudes and the NPM transition.
The signature of a transition function callback is:
def callback(cls, date, transitions, state, idx):
It has access to the current state date, the complete list of transitions, the current state, the current index into the transitions list, and any other keyword args in the transition that were inserted when transitions were set. This function can add downstream transitions or directly update the current state.
User-defined states¶
One of the driving factors in the design of the commanded states module is making it easy
for users to create custom states with minimal effort. With the available base classes,
and in particular FixedTransition
and
ParamTransition
, it is often just a few lines of code.
For example, if we were interested in the state of the IU mode select, we look at examples
of the relevant command, which in this case is CIMODESL
.
>>> cmds = commands.get_cmds('2017:360', '2018:001', tlmsid='CIMODESL')
>>> cmds[0]
<Cmd 2017:360:14:05:00.000 COMMAND_HW tlmsid=CIMODESL scs=128 step=2 timeline_id=426102971 hex=7C063C0 msid=CIU1024T>
Here we see that the IU mode state value is captured in the msid
parameter. That tells
us this can be implemented as a ParamTransition
sub-class.
This class is documented, but in practice it is probably easiest to look through the available
classes in the code and find an example. In this case the ObsidTransition
class:
class ObsidTransition(ParamTransition):
"""Obsid update"""
command_attributes = {'type': 'MP_OBSID'}
state_keys = ['obsid']
transition_key = 'obsid'
cmd_param_key = 'id'
So we just adapt this:
>>> from kadi.commands.states import ParamTransition
>>> class IUModeSelectTransition(ParamTransition):
... """IU mode select update"""
... command_attributes = {'tlmsid': 'CIMODESL'}
... state_keys = ['iu_mode_select']
... transition_key = 'iu_mode_select'
... cmd_param_key = 'msid'
Notes:
Just by running this code, the state is automatically registered and becomes part of the system.
The
command_attributes
class attribute selects which commands will cause this state transition. This attribute is relevant for most transition classes.The
state_keys
attribute indicates when this class will be included in processing. In other words, when the user requests states or continuity, they generally provide astate_keys
argument specifying which keys are desired. If the userstate_keys
overlaps with the classstate_keys
, then this class will be processed.The
transition_key
andcmd_param_key
attributes are specific to theParamTransition
base class and indicate the name of the state to update using the value of the specified command parameter, respectively.
Note
The transition_key
must be in the state_keys
list, but not vica-versa.
The subtlety here is that if you have multiple transition classes that affect multiple
states as a group, all the transition classes need to have the same state_keys
attribute. An example of this is in the grating transition classes, e.g.:
class HETG_INSR_Transition(FixedTransition):
"""HETG insertion"""
command_attributes = {'tlmsid': '4OHETGIN'} # Command (HETG insert)
state_keys = ['letg', 'hetg', 'grating'] # Collective set of grating states
transition_key = ['hetg', 'grating'] # States that *this* class sets
transition_val = ['INSR', 'HETG'] # Corresponding fixed values
So now with our new IUModeSelectTransition
class defined, we can use it!
>>> states.get_continuity('2018:001', state_keys='iu_mode_select')
{'__dates__': {'iu_mode_select': '2018:001:02:30:00.000'},
'iu_mode_select': 'CIU1024T'}
>>> states.get_states('2018:001', '2018:004', state_keys='iu_mode_select')
<Table length=19>
datestart datestop iu_mode_select trans_keys
str21 str21 str8 object
--------------------- --------------------- -------------- --------------
2018:001:12:00:00.000 2018:001:12:45:00.000 CIU1024T
2018:001:12:45:00.000 2018:001:19:45:00.000 CIU1024X iu_mode_select
2018:001:19:45:00.000 2018:002:02:00:00.000 CIU1024X iu_mode_select
2018:002:02:00:00.000 2018:002:11:20:00.000 CIU1024T iu_mode_select
2018:002:11:20:00.000 2018:002:19:00:00.000 CIU1024X iu_mode_select
2018:002:19:00:00.000 2018:002:19:12:00.000 CIU512T iu_mode_select
2018:002:19:12:00.000 2018:002:19:21:50.000 CIMODESL iu_mode_select
2018:002:19:21:50.000 2018:002:19:55:00.000 CIU512T iu_mode_select
2018:002:19:55:00.000 2018:002:20:04:50.000 CIMODESL iu_mode_select
2018:002:20:04:50.000 2018:002:20:38:00.000 CIU512T iu_mode_select
2018:002:20:38:00.000 2018:002:20:47:50.000 CIMODESL iu_mode_select
2018:002:20:47:50.000 2018:002:21:21:00.000 CIU512T iu_mode_select
2018:002:21:21:00.000 2018:002:21:30:50.000 CIMODESL iu_mode_select
2018:002:21:30:50.000 2018:002:22:04:00.000 CIU512T iu_mode_select
2018:002:22:04:00.000 2018:002:22:13:50.000 CIMODESL iu_mode_select
2018:002:22:13:50.000 2018:003:11:10:00.000 CIU512T iu_mode_select
2018:003:11:10:00.000 2018:003:19:35:00.000 CIU1024X iu_mode_select
2018:003:19:35:00.000 2018:004:01:00:00.000 CIU1024T iu_mode_select
2018:004:01:00:00.000 2018:004:12:00:00.000 CIU1024T iu_mode_select
Sometimes the pre-defined base classes are not enough, and in these cases the main
challenge is typically defining the set_transitions()
method and potentially defining
transition callback functions. There are a number of examples of this in the kadi code
and this should serve as your starting point. The Ska team will be happy to assist
you if this is not enough.