Tutorial
========

For the impatient, see the `Quickstart`_ section below.  To take full advantage of all
the capabilities of annie, continue on through the rest of the tutorial as well.

Quickstart
-----------

The very simplest way to run an annie simulation is to take some or all
of the simulation parameters from an existing or planned observation via the starcheck
catalog.  This is done with the :func:`~annie.annie.run_from_starcheck` function::

  >>> from annie import run_from_starcheck

  >>> obsid = 20201
  >>> stop = 200  # seconds.  Can also do `stop = 200 * u.s` with astropy units.

  >>> sc = run_from_starcheck(obsid, verbose=True, stop=stop)
  Running annie with:
  {'att_cmd': [193.228633, -63.884565, 39.69144],
   'ccd_bgd_data': '2018:051:02:57:08.203',
   'dither': {'pitch_ampl': 8.0,
              'pitch_period': 707.1,
              'pitch_phase': 0.0,
              'yaw_ampl': 8.0,
              'yaw_period': 1000.0,
              'yaw_phase': 0.0},
   'starcat': <Table length=12>
  sc_id obsid  idx   slot     id     idnote ...  yang  zang  dim   res  halfw pass notes
  int64 int64 int64 int64   int64    object ... int64 int64 int64 int64 int64 str2  str2
  ----- ----- ----- ----- ---------- ------ ... ----- ----- ----- ----- ----- ---- -----
   2116 20201     1     0          1   None ...   919  -844     1     1    25   --    --
   2116 20201     2     1          5   None ... -1828  1053     1     1    25   --    --
   2116 20201     3     2          6   None ...   385  1697     1     1    25   --    --
   2116 20201     4     3 1178736784   None ...  2004  -622    28     1   160   a2    --
   2116 20201     5     4 1178737152   None ...   775  1438    32     1   180   --    --
   2116 20201     6     5 1178737496   None ...  1219   123    32     1   180   --    --
   2116 20201     7     6 1179259616   None ... -2112 -1949    28     1   160   a2    --
   2116 20201     8     7 1178736896   None ...  2420  -905     1     1    25   --    --
   2116 20201     9     7 1179257400   None ...    33   544    32     1   180   --    --
   2116 20201    10     0 1179257016   None ...    -8 -2234    29     1   165   --    --
   2116 20201    11     1 1179257288   None ...   942  -257    28     1   160   a2    --
   2116 20201    12     2 1179127856   None ... -1782   -21    28     1   160   a2    --,
   'stop': 200,
   't_ccd': -11.4}

   >>>

The :func:`~annie.annie.run_from_starcheck` function can also take as input the starcheck
dict returned from `get_starcheck_catalog()
<http://cxc.cfa.harvard.edu/mta/ASPECT/tool_doc/mica/api.html#mica.starcheck.get_starcheck_catalog>`_,
or the text copied from the starcheck report.  In the latter case one could easily update
catalog parameters to evaluate a potential catalog update.

In addition one can override any of the :meth:`~annie.annie.Spacecraft.track_stars_setup`
parameters for the observation, for instance simulate this observation at a higher CCD
temperature::

  >>> sc = run_from_starcheck(obsid, t_ccd=-5.0, stop=stop)

Now to look at the results, see the section on `Exploring annie's telemetry`_.

Creating spacecraft subsystems
------------------------------

All spacecraft subsystems required to run a simulation are created using the :class:`~annie.annie.Spacecraft`
master controller::

  >>> from annie.annie import Spacecraft
  >>> sc = Spacecraft()

The main s/c subsystems include the :class:`~annie.clock.Clock`, :class:`~annie.sky.Sky`, :class:`~annie.aca.ACA`,
:class:`~annie.aca.CCD`, :class:`~annie.pcad.PCAD` and :class:`~annie.telem.Telem` objects::

  >>> sc.__dict__
  {'aca': <annie.aca.ACA at 0x7f50a85ca630>,
   'ccd': <annie.aca.CCD at 0x7f50a85c4198>,
   'clock': <Clock secs=0.000 ticks=0>,
   'logger': <Logger root (CRITICAL)>,
   'loglevel': 50,
   'pcad': <annie.pcad.PCAD at 0x7f50a85ca7f0>,
   'sky': <annie.sky.Sky at 0x7f50a85ca390>,
   'start': 0,
   'stop': 100,
   'telem': <annie.telem.Telem at 0x7f50a85ca438>}


Internal clock
--------------

The annie clock units are as follows:

===========  ========== ======= ============
Name         Attribute   Ticks   Seconds
===========  ========== ======= ============
Clock tick   ``tick``      1     0.016015625
Minor cycle  ``mnc``       1     0.016015625
Minor frame  ``mnf``      16     0.25625
Frame        ``frame``    64     1.025
Major frame  ``mjf``    2048     32.8
===========  ========== ======= ============

One frame lasts 1.025 seconds and there are 64 clock ticks per frame::

  >>> from annie.clock import TICKS_PER_FRAME
  >>> print(TICKS_PER_FRAME)
  64

The relative durations of these clock units is available in the ``TICKS_PER``
module dictionary::

  >>> from annie.clock import TICKS_PER
  >>> print(TICKS_PER.keys())
  dict_keys(['mnc', 'mnf', 'mjf', 'frame', 'sec'])

  >>> print(TICKS_PER['mnf'])
  16

  >>> print(TICKS_PER['sec'])
  62.43902439024391

The clock module contains two classes, :class:`~annie.clock.ClockTime` and :class:`~annie.clock.Clock`.

ClockTime
^^^^^^^^^

The :class:`~annie.clock.ClockTime` class provides functionality to convert Date or DateTime objects
into the internal annie clock time::

  >>> from annie.clock import ClockTime
  >>> import astropy.units as u

  >>> ct = ClockTime(15)  # Initialize in ticks
  >>> ct
  <ClockTime secs=0.240, ticks=15>

  >>> ClockTime(32.8 * u.s)  # Initialize in seconds
  <ClockTime secs=32.800, ticks=2048>

  >>> ClockTime('2017:001')  # Absolute date
  <ClockTime secs=599616069.191 ticks=37439442369>

It also provides functionality to manipulate the internal time, e.g. derive the time at the next unit::

  >>> ct.at_next('frame')
  <ClockTime secs=1.025, ticks=64>

or derive the number of ticks that have passed since the start of a given unit::

  >>> ct = ClockTime(41 * u.s)
  >>> ct
  <ClockTime secs=41, ticks=2560>

  >>> ct.ticks_mod('mjf')
  512  # 2560 - 2048

or perform arithmetic operations on the :class:`~annie.clock.ClockTime` objects::

  >>> ct_delta = ClockTime(2)
  >>> ct + ct_delta
  <ClockTime secs=41.032, ticks=2562>

Clock
^^^^^

The :class:`~annie.clock.Clock` class controls the internal clock. It inherits from the :class:`~annie.clock.ClockTime`
class. It also inherits from the :class:`~annie.subsystem.SubSystem` class, and thus annie's clock is one of the
spacecraft subsystems created within the :class:`~annie.annie.Spacecraft` master controller for the simulation::

  >>> from annie.clock import Clock
  >>> from annie.annie import Spacecraft
  >>> import astropy.units as u

  >>> sc = Spacecraft()

  >>> c = sc.clock
  >>> c.start = 0         # ticks

  >>> c                   # Clock object
  <Clock secs=0.000, ticks=0>

  >>> c.start             # ClockTime object
  <ClockTime secs=0.00, ticks=0>

The end of the simulation is specified by setting the ``stop`` attribute::

  >>> c.stop = 100 * u.s  # Ticks or delta time via astropy units

The simulation progresses via successive calls to the :meth:`~annie.clock.Clock.tick`
method until the ``stop`` time is reached, at which point the method returns ``False``.

  >>> # The first tick() method call initializes/starts the clock
  >>> c.tick()
  True
  >>> c
  <Clock secs=0.000, ticks=0>
  >>>
  >>> # The next and following tick() calls move the clock by 1 tick
  >>> c.tick()
  True
  >>> c
  <Clock secs=0.016, ticks=1>


Scheduling actions
------------------

Tasks
^^^^^

In each subsystem there are certain tasks that are executed regularly by the generic
subsystem processing called every tick (:meth:`annie.subsystem.SubSystem.process`,
see :meth:`annie.annie.Spacecraft.run` and :ref:`Running-an-annie-simulation`). Information about
these tasks can be accessed through the tasks attribute, which is a list of tuples containing
method, time unit, and tick offset within this time unit.

For example, the PCAD subsystem performs attitude updates at the start of every minor
frame and main processing at the start of every frame, while the ACA subsystem performs
main processing one tick (one minor cycle) after the start of each frame::

  >>> sc.pcad.tasks
  [('update_att', 'mnf', 0), ('main_processing', 'frame', 0)]

  >>> sc.aca.tasks
  [('main_processing', 'frame', 1)]


Commands
^^^^^^^^

Commands executed by the subsystems are scheduled using the subsystem's
:meth:`~annie.subsystem.SubSystem.command` method which adds the new commands
to the subsystem's :attr:`~annie.subsystem.SubSystem.commands` dictionary::

  >>> sc.pcad.commands
  defaultdict(list, {})

  >>> sc.pcad.pending_commands
  []

  >>> sc.pcad.command('set_att_cmd', 0 * u.s, att=[0., 0., 0.])
  >>> sc.pcad.command('set_att_cmd', 100 * u.s, att=[10., 20., 0.])

  >>> sc.pcad.commands
  defaultdict(list,
              {<ClockTime secs=0.016 ticks=1>: [('set_att_cmd',
                {'att': [0.0, 0.0, 0.0]})],
               <ClockTime secs=100.018 ticks=6245>: [('set_att_cmd',
                {'att': [10.0, 20.0, 0.0]})]})

  >>> sc.pcad.pending_commands
  []

  >>> print(sc.pcad.att_record.att_cmd)
  None

The commands to be executed at this time are then added to the subsystem's
:attr:`~annie.subsystem.SubSystem.pending_commands` list by the generic
subsystem processing called every tick (:meth:`~annie.subsystem.SubSystem.process`,
see :ref:`Running-an-annie-simulation`)::

  >>> sc.clock
  <Clock secs=0.016, ticks=1>

  >>> sc.pcad.process()

  >>> sc.pcad.commands
  defaultdict(list,
              {<ClockTime secs=0.016 ticks=1>: [('set_att_cmd',
                {'att': [0.0, 0.0, 0.0]})],
               <ClockTime secs=100.018 ticks=6245>: [('set_att_cmd',
                {'att': [10.0, 20.0, 0.0]})]})

  >>> sc.pcad.pending_commands
  [('set_att_cmd', {'att': [0.0, 0.0, 0.0]})]

  >>> print(sc.pcad.att_record.att_cmd)
  None

Finally, the commands are executed at their scheduled time as part of the subsystem's
regularly scheduled tasks (e.g. :meth:`annie.pcad.PCAD.main_processing` which calls
subsystem's :meth:`~annie.subsystem.SubSystem.execute_pending_commands` method).
This removes the command from the pending commands list. However, the command is kept
in the commands list which acts as a log of commands scheduled during the simulation::

  >>> sc.pcad.main_processing()

  >>> sc.pcad.commands
  defaultdict(list,
              {<ClockTime secs=0.016 ticks=1>: [('set_att_cmd',
                {'att': [0.0, 0.0, 0.0]})],
               <ClockTime secs=100.018 ticks=6245>: [('set_att_cmd',
                {'att': [10.0, 20.0, 0.0]})]})

  >>> sc.pcad.pending_commands
  []

  >>> print(sc.pcad.att_record.att_cmd)
  <Quat q1=0.00000000 q2=-0.00000000 q3=0.00000000 q4=1.00000000>

The next scheduled command gets processed and executed when the clock reaches its
scheduled time::

  >>> sc.clock.ticks = 6245

  >>> sc.pcad.process()

  >>> sc.pcad.commands
  defaultdict(list,
              {<ClockTime secs=0.016 ticks=1>: [('set_att_cmd',
                {'att': [0.0, 0.0, 0.0]})],
               <ClockTime secs=100.018 ticks=6245>: [('set_att_cmd',
                {'att': [10.0, 20.0, 0.0]})]})

  >>> sc.pcad.pending_commands
  [('set_att_cmd', {'att': [10.0, 20.0, 0.0]})]

  >>> sc.pcad.main_processing()

  >>> sc.pcad.commands
  defaultdict(list,
              {<ClockTime secs=0.016 ticks=1>: [('set_att_cmd',
                {'att': [0.0, 0.0, 0.0]})],
               <ClockTime secs=100.018 ticks=6245>: [('set_att_cmd',
                {'att': [10.0, 20.0, 0.0]})]})

  >>> sc.pcad.pending_commands
  []

  >>> print(sc.pcad.att_record.att_cmd)
  <Quat q1=0.01513444 q2=-0.17298739 q3=0.08583165 q4=0.98106026>


Events
^^^^^^

Events to be executed at a given time are added to the subsystem's events queue
(:attr:`annie.subsystem.SubSystem.events`) using the subsystem's
:meth:`annie.subsystem.SubSystem.add_event` method::

  >>> # Schedule a `flush` event (change the ccd status to `flush`)
  >>> sc.clock.ticks = 0  # reset the clock for the purpose of this example

  >>> sc.ccd.events
  defaultdict(list, {})

  >>> sc.ccd.add_event('flush',  sc.clock + 1 * u.mnc)

  >>> sc.ccd.events
  defaultdict(list, {<ClockTime secs=0.016 ticks=1>: [('flush', {})]})

The events in the events queue are executed at their scheduled time by the generic
subsystem processing called every tick (:meth:`annie.subsystem.SubSystem.process`,
see :ref:`Running-an-annie-simulation`). The execution of the event does not remove
the event from the events list which acts as a log of events scheduled for a given
subsystem in the course of the simulation::

  >>> sc.ccd.status
  `idle`

  >>> sc.clock.tick()
  >>> sc.clock
  <Clock secs=0.016 ticks=1>

  >>> sc.ccd.process()
  >>> sc.ccd.status
  `flush`

  >>> sc.ccd.events
  defaultdict(list, {<ClockTime secs=0.016 ticks=1>: [('flush', {})]})


.. _annie-setup:

Setting up an annie simulation
------------------------------

Setting up an annie simulation requires providing a number of subsystem
parameters and issuing relevant commands.  This is most conveniently done
using the :meth:`annie.annie.Spacecraft.track_stars_setup`
method. By convention, simulations normally start at time=0,
and the end of the simulation is defined with a `stop` parameter (which then
also corresponds to the duration).
Once the parameters are set, the :meth:`~annie.annie.Spacecraft.track_stars_setup`
method calls PCAD's :meth:`annie.pcad.PCAD.track_guide_stars` method which commands
ACA to search for the guide stars and start guiding.

The simulation parameter fall into **four categories**:
 * Stars (sky and catalog),
 * Attitude,
 * Conditions,
 * Image and image processing.

----

.. |sky| image:: images/sky.png
   :width: 140px
   :align: middle

.. |attitude| image:: images/attitude.png
   :width: 140px
   :align: middle

.. |conditions| image:: images/conditions.png
   :width: 140px
   :align: middle

.. |image| image:: images/image.png
   :width: 140px
   :align: middle


.. list-table::
   :header-rows: 0
   :widths: 10 60

   * - |sky|
     -
       * `stars_source`, `stars_data` - the stars available in the ACA field of view are fetched from the AGASC if
         ``stars_source='agasc'`` and ``stars_data=None``. Alternatively, the user may build an arbitrary synthetic
         sky star field, e.g. to test a scenario with a spoiler star nearby a guide star. In this case the setup
         should read ``stars_source='table'`` and `stars_data` should be an astropy Table with columns: ``id, yag, zag, mag``.
       * `starcat` parameter defines the catalog with the guide stars to be tracked. Should be supplied as an astropy Table. See also :ref:`Running-an-annie-simulation`.
   * - |attitude|
     -
       * `att_source`, `att_data` - by default the attitudes for an annie run are simulated
         (``att_source='simulation'``, ``att_data=None``) based on the initial (commanded)
         attitude, dither pattern and attitude update control law. However, for the sake
         of flight data validation, it is possible to use a list of on-board or ground
         quaternions (``att_source=('telemetry'|'ground')``, ``att_data=<list of quaternions>``)
       * `att_cmd` - The commanded attitude must be provided as ``att_cmd=[ra, dec, roll]``
       * `att_est` - estimated attitude
       * `att_true` - true attitude
       * `dither` - The user may provide the dither pattern as
         a dictionary, or use the default (standard ACIS 8 arcsec dither).

   * - |conditions|
     -
       * `t_ccd_ref`, `t_ccd` - parameter `t_ccd_ref` is used to set the reference temperature
         corresponding to the conditions at the time of an ACA DCC ``ccd_bgd_source='DCC'``,
         or the temperature corresponding to a synthetic dark background
         (``ccd_bgd_source='Custom_1024x1024'``). The user may trigger temperature scaling
         of the background data by defining the CCD temeprature `t_ccd`
       * `sp_flags`, `ms_flags`, `dp_flags`, `ir_flags` - the user may provide various flags
         (SP - saturated pixel, MS - multiple stars, DP - defective pixel, IR - ionizing radiation)
         and test their impact on the ACA tracking the stars. The flags are provided as dicts
         of lists ordered by the slot number.
       * TODO: proper testing, simulating the flags.

   * - |image|
     -
       * `img_source`, `img_data` - by default the star images are simulated
         (``img_source='simulation'`` and ``img_data=None``). However, the user may
         choose to 'shine' the flight data for the purpose of comparing the simulation
         and the telemetry. In this case the input should read ``img_source='telemetry'``
         and ``img_data=<list of flight images>``
       * `ccd_bgd_source`, `ccd_bgd_data` - by default the simulator uses dark background
         derived from the ACA DCC map taken at the date nearest to the date of the simulation.
         However, the user may choose from the number of custom options to replace the default
         background with an arbitrary background (see :meth:`~annie.annie.Spacecraft.track_stars_setup`
         and :meth:`~annie.aca.CCD.set_ccd_bgd_data` for details)
       * `bgd_algorithm` - this parameter sets the background subtraction algorithm

**Background setup examples**::

  >>> # No background, will use ccd_bgd_data = 0.
  >>> bgd_algorithm = 'Constant'
  >>> ccd_bgd_source = None

  >>> # Constant background
  >>> bgd_algorithm = 'Constant'
  >>> ccd_bgd_source = 'Constant'
  >>> ccd_bgd_data = 20.

  >>> # Flight algorithm and bgd data
  >>> bgd_algorithm = 'FlightBgd'
  >>> ccd_bgd_source = 'telemetry'
  >>> ccd_bgd_data = <list of telemetered BGDAVG values>

  >>> # Flight algorithm and ACA DCC bgd data
  >>> bgd_algorithm = 'FlightBgd'
  >>> ccd_bgd_source = 'DCC'
  >>> ccd_bgd_data = '2017:100'    # fetch the ACA DCC nearest to this date

  >>> # Dynamic bgd algorithm and ACA DCC bgd data
  >>> bgd_algorithm = 'DynamBgd'
  >>> % ccd_bgd_source = 'DCC'       # use the default setting for ccd_bgd_data = the date of the simulation

  >>> # Dynamic bgd algorithm and custom 1024x1024 DCC map scaled with temperature
  >>> bgd_algorithm = 'DynamBgd'
  >>> ccd_bgd_source = 'Custom_1024x1024'
  >>> ccd_bgd_data = <custom 1024x1024 ndarray or ACAImage>
  >>> t_ccd_ref = -14              # indicate that the custom bgd map was computed at -14C
  >>> t_ccd = -5                   # scale it with temperature to -5C


.. _Running-an-annie-simulation:

Running an annie simulation
---------------------------

Once all parameters are set and the stars are found and identified,
the simulation is ready to be performed. This is done using the
:meth:`annie.annie.Spacecraft.run` method, which initializes the
annie clock and processes all the s/c subsystems at each clock tick,
until the clock reaches the stop time specified by the user.

The **minimal setup** requires the following settings:
 * the stop time of the simulation (`stop` parameter), and
 * a star catalog (an astropy Table, `starcat` parameter)
 * a commanded attitude.

With this minimum setup, the system will perform a full simulation (image and attitude
data will be simulated). Image processing will be performed assuming the ACA DCC
background taken at the date closest to the date of the simulation, and the flight
algorithm will be used to subtract the background (average bgd value derived from
the eight corner pixels)::

  >>> # Example 1 (This uses the get_att, get_starcat, and get_dither helper methods from mica.starcheck)

  >>> from annie.annie import Spacecraft
  >>> import astropy.units as u
  >>> from mica.starcheck import get_att, get_starcheck, get_dither

  >>> sc = Spacecraft()

  >>> stop = 20 * u.s
  >>> obsid = 8008

  >>> sc.track_stars_setup(stop=stop,
                           att_cmd=get_att(obsid),
                           starcat=get_starcat(obsid),
                           dither=get_dither(obsid))
  >>> sc.run()

or::

  >>> # Example 2

  >>> from annie.annie import Spacecraft
  >>> import astropy.units as u
  >>> from astropy.table import Table

  >>> sc = Spacecraft()

  >>> stop = 20 * u.s
  >>> starcat = Table.read("""
    slot type sz mag maxmag yang zang halfw
    0 BOT 8x8 10.1 11.5 0.0 0.0 120
    1 BOT 8x8 10.1 11.5 1000.0 0.0 120
    2 BOT 8x8 10.1 11.5 0.0 1000.0 120
    3 BOT 8x8 10.1 11.5 -1000.0 0.0 120
    """, format='ascii', guess=False)

  >>> sc.track_stars_setup(stop=stop,
                           starcat=starcat,
                           stars_source='table',
                           stars_data=starcat,
                           att_cmd=[0., 0., 0.])
  >>> sc.run()


Exploring annie's telemetry
---------------------------

Telemetry is collected by the :mod:`~annie.telem` module by the
means of regularly executed :attr:`~annie.telem.Telem.tasks`
defined for the :class:`~annie.telem.Telem` class.
These tasks include (see :ref:`telem-subsystem`):

  * :meth:`~annie.telem.Telem.main_processing` which is called every
    frame; it collects

     * the :class:`~annie.pcad.SlotRecord` telemetry for each slot
       and stores it in :attr:`annie.telem.Telem.slot_records`,
     * the :class:`~annie.aca.StarDataRecord` telemetry for each slot
       and stores it in :attr:`annie.telem.Telem.star_data_records`,

  * :meth:`~annie.telem.Telem.attitude_processing` which is called every
    minor frame; it collects the :class:`~annie.pcad.AttitudeRecord` telemetry
    and stores it in :attr:`annie.telem.Telem.att_records`.

Telemetry tables
^^^^^^^^^^^^^^^^

The spacecraft ``telem`` object has three sub-attributes that contain most of the
useful output from an annie simulation.  These are mostly just tabularized versions
the full telemetry described above.

   * ``sc.telem.aca_slots``: dict of ACA :class:`~annie.aca.StarDataRecord` tables keyed by slot
   * ``sc.telem.pcad_slots``: dict of PCAD star :class:`~annie.pcad.SlotRecord` info tables () keyed by slot
   * ``sc.telem.pcad_att``: PCAD :class:`~annie.pcad.AttitudeRecord` info table

These attributes are the recommended way to access the annie simulation results.

The example below shows how to access these attributes to plot the y-angle centroids.
In most cases the telemetry from the first 8.2 seconds of simulation are
not interesting (where acquisition occurs), so we can just clip that using the
:meth:`~annie.telem.Telem.clip` method::

   >>> from annie import run_from_starcheck
   >>> sc = run_from_starcheck(20201, stop=700)
   >>> sc.telem.clip()

Now let's plot the telemetry:

   >>> import matplotlib.pyplot as plt
   >>> %matplotlib

   >>> aca_slot = sc.telem.aca_slots[3]
   >>> plt.plot(aca_slot['time'], aca_slot['zag'])
   >>> plt.xlabel('Time (sec)')
   >>> plt.grid()
   >>> plt.title('Z-angle (arcsec)')

.. image:: images/yag_example.png
   :width: 320px


The next example shows how to compute the offsets between
the true and estimated attitudes::

   >>> times = sc.telem.pcad_att['time']
   >>> atts_true = sc.telem.pcad_att['att_true']
   >>> atts_est = sc.telem.pcad_att['att_est']
   >>> dr = []
   >>> dp = []
   >>> dy = []
   >>> for att_true, att_est in zip(atts_true, atts_est):
   >>>     dq = att_true.dq(att_est)
   >>>     dr.append(dq.roll0 * 3600)
   >>>     dy.append(dq.yaw * 3600)
   >>>     dp.append(dq.pitch * 3600)
   >>>
   >>> plt.plot(times, dr)
   >>> plt.ylabel('offset (arcsec)')
   >>> plt.xlabel('Time (s)')
   >>> plt.grid()
   >>> plt.title('Delta roll')

.. image:: images/droll_example.png
   :width: 320px


Slot records
^^^^^^^^^^^^

The telemetry records are dictionaries keyed by slot number, with values
that are lists containing the relevant records ordered by time. The example
below shows how to access the slot record telemetry that stores the
:data:`~annie.pcad.GS_loss_count` counter Kalman status, star residuals,
star magnitude, slot number, ACA function and a time stamp for each frame::

  >>> sc.telem.slot_records.keys()
  dict_keys([3, 4, 5, 6, 7])

  >>> # The first two slot records for slot number 3
  >>> sc.telem.slot_records[3][:2]
  [<SlotRecord: slot=3 time=0.0 F_image=NULL_IMAGE>,
   <SlotRecord: slot=3 time=1.025 F_image=NULL_IMAGE>]

  >>> # Explore the content of an individual slot record
  >>> sr = sc.telem.slot_records[3][10] # 10th record for slot number 3
  >>> print(sr)
  {'F_image': 'STAR',
   'GS_loss_count': 0,
   'Kalman_ok': True,
   'clock': <ClockTime secs=10.250 ticks=640>,
   'delta_y': 0.040003284291393326,
   'delta_z': 0.076399486542315209,
   'function': 'TRAK',
   'mag': 9.2567258517843527,
   'slot': 3,
   'time': 10.249999999999998}

Plot the image function for slot number 7::

   >>> import numpy as np
   >>> F_images = np.array([sr.F_image for sr in sc.telem.slot_records[7]])
   >>> times = np.array([sr.time for sr in sc.telem.slot_records[7]])
   >>> state_codes = [(0, 'NULL_IMAGE'), (1, 'STAR')]
   >>> vals = np.zeros_like(F_images)
   >>> for state_code in state_codes:
   ...     ok = F_images == state_code[1]
   ...     vals[ok] = state_code[0]

   >>> from Ska.Matplotlib import plot_cxctime
   >>> plot_cxctime(times, vals, 'o--', state_codes=state_codes)

.. image:: images/F_image_example.png
   :width: 400px


.. _`star-data-record`:

Star data records
^^^^^^^^^^^^^^^^^

To explore the star data use :attr:`~annie.telem.Telem.star_data_records`. Note
that the star data record telemetry starts at ``t = 1.025 sec``, as opposed
to the slot record telemetry that starts at ``t = 0 sec``::

  >>> # Explore star data telemetry resulting from Example 1 above
  >>> sc.telem.star_data_records.keys()
  dict_keys([3, 4, 5, 6, 7])

  >>> # The first two star data records for slot number 3
  >>>sc.telem.star_data_records[3][:2]
  [<StarDataRecord: slot=3 time=1.025 row0=46 col0=222>,
   <StarDataRecord: slot=3 time=2.05 row0=511 col0=511>]

  >>> # Explore the content of an individual slot record
  >>> sdr = sc.telem.star_data_records[3][10] # 10th record for slot number 3
  >>> print(sdr)
  {'bgd': 17.0,
  'bgd_avg': 17.0,
  'bgd_rms': 13.714108945658715,
  'bgd_status': array([ True, False,  True,  True,  True,  True,  True, False], dtype=bool),
  'brightest': True,
  'clock': <ClockTime secs=11.275 ticks=704>,
  'col': 245.49913455685868,
  'col1': 250,
  'fid': False,
  'function': 'TRAK',
  'halfwidth': 120,
  'image': <ACAImage row0=66 col0=242
  array([[  8,  45,   5,  35, 119,   7,   5,   5],
        [ 27,  89,   7,   5,  13,   5,  30,  35],
        [ 41,   6,  23,  56, 121,  39,  17,   3],
        [ 95,  12, 161, 479, 461, 198,  14,  11],
        [  6,  17, 188, 833, 863, 220,  44,  22],
        [ 51,  12, 107, 397, 412, 108,  70, 165],
        [ 28,  23,  43, 161,  69,  56,  40,  69],
        [ 44,  35,  25,   9,  12,  24,  18,  37]])>,
  'img_sum': 4753.3399205036621,
  'last_sdr': <weakref at 0x7fa1325a6db8; to 'StarDataRecord' at 0x7fa1325b37b8>,
  'mag': 9.2567258524020062,
  'maxmag': 10.859,
  'min_img_sum': 543.32441108392015,
  'rate_c': 0.027156531569687559,
  'rate_r': -0.023609485876363578,
  'repeat_count': 0,
  'row': 69.972698185568873,
  'row1': 74,
  'search_count': 4,
  'size': 6,
  'slot': 3,
  'threshold': 10.859,
  'time': 11.274999999999999,
  'yag': -318.80588100432664,
  'yag_cat': -318.30117934550685,
  'zag': 1202.8794098096994,
  'zag_cat': 1202.290747547197}

Plot the ACA function for slot number 3:

   >>> import numpy as np
   >>> funcs = np.array([sdr.function for sdr in sc.telem.star_data_records[3]])
   >>> times = np.array([sdr.time for sdr in sc.telem.star_data_records[3]])
   >>> state_codes = [(0, 'NONE'), (1, 'TRAK'), (2, 'SRCH'), (3, 'RACQ')]
   >>> vals = np.zeros_like(funcs)
   >>> for state_code in state_codes:
           ok = funcs == state_code[1]
           vals[ok] = state_code[0]

   >>> from Ska.Matplotlib import plot_cxctime
   >>> plot_cxctime(times, vals, 'o--', state_codes=state_codes)

.. image:: images/function_example.png
   :width: 400px


Attitude records
^^^^^^^^^^^^^^^^
The :attr:`~annie.telem.Telem.att_records` attribute is a list containing
an :attr:`~annie.pcad.AttitudeRecord` for each minor frame. It allows for exploration
of the spacecraft attitude during the simulation. It contains the
time history of the commanded, true and estimated attitudes, their yaw,
pitch and roll components, and dither, true and estimated rates::

   >>> # Access the 10th attitude record
   >>> ar = sc.telem.attitude_records[10]
   >>> print(ar)
   {'att_cmd': <Quat q1=0.14961427 q2=0.49089671 q3=0.83147065 q4=0.21282047>,
    'att_est': <Quat q1=0.14961408 q2=0.49089675 q3=0.83147077 q4=0.21282004>,
    'att_true': <Quat q1=0.14961406 q2=0.49089675 q3=0.83147078 q4=0.21281999>,
    'clock': <ClockTime secs=2.562 ticks=160>,
    'last_att_record': <weakref at 0x7fa1325800e8; to 'AttitudeRecord' at 0x7fa1325e3ac8>,
    'pitch_cmd': 0.0,
    'pitch_dither': 0.18214420697121447,
    'pitch_dither_rate': 0.071068381251204654,
    'pitch_err': -0.018200254323126025,
    'pitch_est': 0.16394395264808845,
    'pitch_est_rate': 0.07108680873629852,
    'pitch_true': 0.18401325204517213,
    'pitch_true_rate': 0.071978393967360957,
    'roll_cmd': 0.0,
    'roll_dither': 0.0,
    'roll_dither_rate': 0.0,
    'roll_err': 0.0,
    'roll_est': 0.0,
    'roll_est_rate': 0.0,
    'roll_true': 0.0,
    'roll_true_rate': 0.0,
    'time': 2.5624999999999996,
    'yaw_cmd': 0.0,
    'yaw_dither': 0.12879973380786744,
    'yaw_dither_rate': 0.050258967404293226,
    'yaw_err': -0.0128749648904041,
    'yaw_est': 0.11592476891746334,
    'yaw_est_rate': 0.05026548245743669,
    'yaw_true': 0.13012066185289703,
    'yaw_true_rate': 0.050902715648813428}


Kalman tracking
^^^^^^^^^^^^^^^^

The Kalman status and the number of Kalman stars are collected
for each frame and stored in :attr:`annie.telem.Telem.Kalman_status` and
:attr:`annie.telem.Telem.n_Kalman_stars`. The example below illustrates that
there were enough Kalman stars in each slot and for each frame once the ACA
function changed from 'SRCH' to 'TRAK' (``Kalman_status = True``;
see :ref:`star-data-record` to learn how to access the ACA function data),
and that the actual number of Kalman stars for each fo these frames was
``n_Kalman_stars = 5``::

  >>> # Explore telemetry resulting from Example 1 above

  >>> # Frame no. 7 is the last frame with ACA function equal 'SRCH'
  >>> # while frame no. 8 is the first frame with ACA function equal 'TRAK'
  >>> sc.telem.Kalman_status[7:9]
  >>> [[False, False, False, False, False], [True, True, True, True, True]]

  >>> # The number of Kalman stars was 5 in all frames
  >>> # with ACA function equal 'TRAK'
  >>> sc.telem.n_Kalman_stars
  >>> [0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]