Design overview
===============

Motivation
----------

The annie package is focused on modeling the ACA behavior
while guiding on stars. The key feature of the design is that it
is modular and extensible, allowing flexibility in supporting
variations of simulation parameters or algorithms such as:

* Algorithm to simulate star image data:
    * Gaussian PSF
    * ACA PSF image data
* Dark CCD background
    * Background from flight ACA dark current calibration
    * Time-history of arbitrarily defined background
    * Constant background
* Background subtraction
    * Flight algorithm based on the eight corner pixels
    * Dynamic background using median-filterd edge pixels
    * Warm pixel detection within each image
* Simulation strategy
    * Pure simulation
    * Plug in flight data for select components such as ACA images, background, or attitude
* Centroiding algorithms

Code overview
-------------

Spacecraft
^^^^^^^^^^

At the top level of the simulator there is a single :class:`~annie.annie.Spacecraft` class
which serves as master controller for an annie simulation.  It performs the following key functions:

* Serves as a container for each of the `Subsystems`_ classes
* Provides the :meth:`~annie.annie.Spacecraft.track_stars_setup` convenience method to set up
  most of the supported configuration options for typical simulations of tracking guide stars.
* Provides the :meth:`~annie.annie.Spacecraft.run` method to actually run a simulation once
  everything is set up.

Running the simulation consists of a loop in which the spacecraft clock is
incremented by one "tick", and then the main ``process()`` method of each subsystem is
called.  One tick is a minor cycle (1.025 sec / 64 = 16.015625 msec).  The subsystem is
responsible for deciding if any action is needed on that tick.  The order of subsystem
processing does matter and thus the details of subsystems are somewhat coupled.  For example it
is assumed that PCAD runs first in order that the attitude is correct for other
subsystems.

Subsystems
^^^^^^^^^^

The core of ``annie`` is the concept of a subsystem.  This is the object which does the
actual work of simulating a subsytem such as the ACA or PCAD in a modular fashion.  A
subsystem can handle the scheduling and processing of regularly recurring tasks, external
commands, and internally generated events.  For details see the `Subsystem details`_
section.

Subsystems contain attributes which may be constants required for runtime processing or
dynamically updated values.

All subsystems are based on the :class:`~annie.subsystem.SubSystem`: base class.  One
important feature of this class is that it provides access to all of the other subsystems
(and corresponding attributes) via a dynamically generated attribute with the name of the
other subsystem.  For instance within the ``ACA`` subsystem class one could access the
current true yaw using ``self.pcad.att_record.yaw_true``.  This cross-communication
facilitates writing simulation code while still maintaining modularity.  Note that there
is a "trust" model in place and it is assumed each subsystem treats other subsystem
attributes as read-only.

The implemented subsystems are:

* :class:`~annie.clock.Clock` : on-board clock subsystem
* :class:`~annie.pcad.PCAD` : PCAD subsystem
* :class:`~annie.aca.ACA` : ACA subsystem (i.e. PEA)
* :class:`~annie.aca.CCD` : CCD subsystem (i.e AC electronics + CCD)
* :class:`~annie.sky.Sky` : sky subsystem to provide stars in ACA field of view
* :class:`~annie.telem.Telem` : telemetry collection subsystem

Data storage
^^^^^^^^^^^^

In order to analyze simulation data, the data need to be collected and made available for
use post-simulation.  This is done via the :class:`~annie.telem.Telem` subsystem, which
puts data into lists of Python records (class objects).  This is not a direct simulation
of actual Chandra telemetry, but instead "pseudo-telemetry" that is convenient for
analysis.

* :class:`~annie.aca.StarDataRecord` : ACA telemetry for each slot, collected each frame
* :class:`~annie.pcad.SlotRecord` : PCAD telemetry for each slot, collected each frame
* :class:`~annie.pcad.AttitudeRecord` : attitude telemetry, collected each minor frame

.. _Attitude-control-law:

Subsystem details
-----------------

The :class:`~annie.subsystem.SubSystem` base class is used by each of the subsystem
classes and provides methods that are responsible for scheduling and processing the tasks,
commands and events that are executed by each subsystem. The key method of this
class that deals with generic processing is called
:meth:`~annie.subsystem.SubSystem.process` and it is executed at each clock tick for each
subsystem.  This is done via the following code in the :meth:`~annie.annie.Spacecraft.run`
method::

  while self.clock.tick():
      self.pcad.process()
      self.sky.process()
      self.aca.process()
      self.ccd.process()
      self.telem.process()

Tasks
^^^^^

Actions that are **executed regularly**, usually at the begining
of each frame, or each minor frame. They are defined in the subsystem's
``task`` attribute which is a list of tuples containing a method, time unit,
and number of clock ticks within this unit at which the task is to be executed. For example,
there are two tasks defined for the telemetry subsystem:
:meth:`~annie.telem.Telem.main_processing` called at the begining of each
frame with the goal of collecting the ACA star data record telemetry,
and :meth:`~annie.telem.Telem.attitude_processing` called at the begining
of each minor frame with the goal of updating the spacecraft attitude
:ref:`telem-subsystem`::

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

   [('main_processing', 'frame', 0), ('attitude_processing', 'mnf', 0)]

Execution of tasks happens at their scheduled time as part of
generic subsystem processing that is called every clock tick
(:meth:`~annie.subsystem.SubSystem.process`). The workflow of
regularly executed tasks is illustrated below: :ref:`pcad-subsystem`,
:ref:`sky-subsystem`, :ref:`aca-subsystem`, :class:`~annie.aca.CCD`,
:ref:`telem-subsystem`.

Commands
^^^^^^^^

Commands are actions that are scheduled to be **executed one time**
as part of the generic subsystem processing. Commands are
scheduled using the subsystem's :meth:`~annie.subsystem.SubSystem.command`
method. The parameters of the :meth:`~annie.subsystem.SubSystem.command`
method include method to be called, time, and method's
`**kwargs`. The time can be given in units that
are absolute (parameter `absolute = True`) or relative (time
unit within the current frame; parameter `absolute=False`,
default).

When the internal clock reaches the specified time, the subsystem's
processing transfers the command from the
:attr:`~annie.subsystem.SubSystem.commands` queue to the
:attr:`~annie.subsystem.SubSystem.pending_commands` list, where
it awaits its execution.

Execution of the pending commands happens as part of the subsystem's main processing,
which is usually called at the beginning of each frame.  For a simple example see the PCAD
:meth:`annie.pcad.PCAD.main_processing` which first calls
:meth:`annie.subsystem.SubSystem.execute_pending_commands` and then does ACA processing.
A more complicated example is :meth:`annie.aca.ACA.main_processing`, which sets
up the full CCD processing cycle along with executing pending commands.

Because commands are queued in the ``pending_commands`` buffer, the time specified when
using the :meth:`~annie.subsystem.SubSystem.command` method does not have to be fine tuned
and equal to the time at which the command needs to be executed.  The command will be
executed at the first appropriate opportunity (as determined by the subsystem) after the
specified command time.  For example::

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

   >>> # Define the Spacecraft and set the clock at tick 61
   >>> # (near the end if the 1st frame).  Set the simulation
   >>> # to end at 1.05 seconds (just after 1st frame).
   >>> sc = Spacecraft()
   >>> c = sc.clock
   >>> c.start = 61  # ticks
   >>> c.stop = 1.05 * u.s

   >>> # Command setting the commanded attitude at absolute clock tick = 62
   >>> sc.pcad.command('set_att_cmd', 62, absolute=True, att=[0., 0., 0.])
   >>> print(sc.pcad.commands)
   defaultdict(list,
            {<ClockTime secs=0.993 ticks=62>: [('set_att_cmd',
               {'att': [0.0, 0.0, 0.0]})]})

   >>> while c.tick():
   >>>     sc.pcad.process()
   >>>     print('Clock ticks: ', c.ticks)
   >>>     print('Pending commands: ', sc.pcad.pending_commands)
   >>>     print('Cmd attitude: ', sc.pcad.att_record.att_cmd)
   >>>     print()

   Clock ticks:  61
   Pending commands:  []
   Cmd attitude:  None

   Clock ticks:  62
   Pending commands:  [('set_att_cmd', {'att': [0.0, 0.0, 0.0]})]
   Cmd attitude:  None

   Clock ticks:  63
   Pending commands:  [('set_att_cmd', {'att': [0.0, 0.0, 0.0]})]
   Cmd attitude:  None

   Clock ticks:  64
   Pending commands:  []
   Cmd attitude:  <Quat q1=0.00000000 q2=-0.00000000 q3=0.00000000 q4=1.00000000>

   Clock ticks:  65
   Pending commands:  []
   Cmd attitude:  <Quat q1=0.00000000 q2=-0.00000000 q3=0.00000000 q4=1.00000000>


In the current implementation commanding is used to set up the commanded,
estimated and true attitudes at the start of a simulation, and command
an ACA :meth:`~annie.aca.ACA.search` from within the :class:`~annie.pcad.PCAD`
class either at the start of the simulation or when a star is lost in the
course of a simulation.

Events
^^^^^^

Events are actions that are scheduled to be **executed one time, at a strictly
specified time**. They are added to the subsystem's
events queue (:attr:`~annie.subsystem.SubSystem.events`) using the
:meth:`~annie.subsystem.SubSystem.add_event` method with parameters:
method to be executed, time of execution (absolute :class:`~annie.clock.ClockTime`),
method's `**kwargs`.

Events in the events queue are executed as part of generic subsystem
processing called every clock tick (:meth:`~annie.subsystem.SubSystem.process`)
as soon as the clock reaches the specified time. For example::

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

   >>> # Define the Spacecraft and set the clock at tick 61
   >>> # (near the end if the 1st frame)
   >>> sc = Spacecraft()
   >>> c = sc.clock
   >>> c.start = 61
   >>> c.stop = 1.05 * u.s

   >>> # Add flush event to be executed in 2 minor cycles (= 2 ticks)
   >>> t_flush = c + 2 * u.mnc
   >>> sc.ccd.add_event('flush', t_flush)
   >>> print(sc.ccd.events)
   defaultdict(<class 'list'>, {<ClockTime secs=1.009 ticks=63>: [('flush', {})]})

   >>> while c.tick():
   >>>     sc.ccd.process()
   >>>     print('Clock ticks: ', c.ticks)
   >>>     print('CCD status: ', sc.ccd.status)
   >>>     print()

   Clock ticks:  61
   CCD status:  idle

   Clock ticks:  62
   CCD status:  idle

   Clock ticks:  63
   CCD status:  flush

   Clock ticks:  64
   CCD status:  flush

   Clock ticks:  65
   CCD status:  flush

In the current implementation, events functionality is used within the
:class:`~annie.aca.ACA` class to schedule ACA and CCD events related
to image processing that are to be executed in the next two frames
providing that the CCD status is `idle` (see :ref:`aca-subsystem`).


Summary
^^^^^^^

+-------------------------------------------+-----------+--------------+------------+
| **Characteristic**                        | **Tasks** | **Commands** | **Events** |
+-------------------------------------------+-----------+--------------+------------+
| Executed regularly                        |    Yes    |     No       |    No      |
+-------------------------------------------+-----------+--------------+------------+
| Get buffered and await execution          |    No     |     Yes      |    No      |
+-------------------------------------------+-----------+--------------+------------+
| Requested time matches the execution time |    Yes    |     No       |    Yes     |
+-------------------------------------------+-----------+--------------+------------+


Workflow of regularly executed tasks
------------------------------------

.. |pcad| image:: images/pcad-subsystem.png
   :width: 600px
   :align: middle

.. |sky| image:: images/sky-subsystem.png
   :width: 600px
   :align: middle

.. |aca| image:: images/aca-subsystem.png
   :width: 600px
   :align: middle

.. |telem| image:: images/telemetry-subsystem.png
   :width: 600px
   :align: middle

.. _pcad-subsystem:

PCAD
^^^^

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

   * - |pcad|
     - **The workflow of tasks executed regularly by the PCAD subsystem is as follows:**
       at the start of each minor frame the PCAD subsystem performs a task to
       update the attitude; see :meth:`~annie.pcad.PCAD.update_att`. Annie uses a
       simple :ref:`Attitude-control-law`. The estimated and true attitudes are updated by
       integrating the estimated and true rates. However, for the minor frames
       whose start coincides with the start of a frame with CCD status equal
       ``'idle'`` (meaning that new star data have just been read;
       see :meth:`~annie.aca.CCD.read` and :ref:`aca-subsystem` subsystem workflow),
       the estimated attitude is updated using the star data and fast (linear approximation)
       attitude solution.

       In addition, the PCAD system performs main processing at the start of every
       frame. This includes execution of pending commands (e.g. setting the
       initial commanded attitude or issuing the ACA search command for lost stars),
       and ACA processing.

       There are two actions performed as part of ACA processing. The first one is
       to keep track of the :attr:`~annie.pcad.SlotRecord.GS_loss_count` counters
       for each slot. A relevant counter gets incremented if the image in the
       corresponding slot is found to be in reacquisition
       (:attr:`~annie.aca.StarDataRecord.function` equals `'RACQ'`) or a bad flag is
       set for this slot. The counter is reset to zero if the image function is
       `'TRAK'` and no bad flags are set. If a :attr:`~annie.pcad.SlotRecord.GS_loss_count`
       counter exceeds :data:`~annie.pcad.GS_LOSS_COUNT_THRESH` (set to 100, i.e. 100
       consequtive frames in ``'RACQ'`` and/or with bad flags), the PCAD subsystem
       commands the ACA subsystem to search for a star in this slot,
       the ACA function gets set to ``'SRCH'``, and the counter is reset to zero.

       The second action performed by the PCAD subsystem as part of the regular ACA
       processing is to identify the guide stars, i.e. to assign a value (either
       ``'STAR'`` or ``'NULL_IMAGE'``) to the :attr:`~annie.pcad.SlotRecord.F_image`
       attributes (one per slot). This is done for all slots by checking if the ``y`` and
       ``z`` angle residuals of an image tracked in a given slot are below the values
       defined with the :data:`~annie.pcad.GUIDE_DELTA_Y_THRESH` and
       :data:`~annie.pcad.GUIDE_DELTA_Z_THRESH` PCAD module constants (each set
       to 20 arcsec), and if the image magnitude does not exceed the bright star
       limit (PCAD's :attr:`~annie.pcad.PCAD.bright_threshold` attribute).

.. _sky-subsystem:

Sky
^^^^

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

   * - |sky|
     - **The workflow of tasks executed regularly by the Sky subsystem is as follows:**
       at the start of each frame the Sky subsystem checks if the current S/C attitude
       (:attr:`~annie.pcad.AttitudeRecord.att_true`) is within 0.1 deg (in each axis)
       of the current sky attitude (:attr:`~annie.sky.Sky.att_sky`).  The sky attitude
       corresponds to the center of the circular star field fetched from the AGASC, and
       is typically set to the commanded attitude. If not (typically after a new commanded
       attitude has been commanded, e.g. while simulating a set of loads, not a single
       pointing) then the Sky attitude is updated and a new star field is fetched either
       from the AGASC or from a user provided astropy Table.

.. _aca-subsystem:

ACA
^^^^

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

   * - |aca|
     - **The workflow of tasks executed regularly by the ACA subsystem is as follows:**
       every frame the ACA subsystem waits 1 minor cycle (1 clock tick) and then starts its
       :meth:`~annie.aca.ACA.main_processing`. This adds the
       :meth:`~annie.aca.ACA.star_data_record_process_per_frame` method to the ACA events
       queue to be executed at the next minor cycle. This method is designed to perform
       processing and setup related to star data records. Currently, it increments
       the :attr:`~annie.aca.StarDataRecord.repeat_count` counter for each slot.
       This counter can be equal 0 or 1, where 0 indicates a frame with new star data.

       The subsequent workflow depends on the CCD status. If CCD status is `'idle'` then
       the :meth:`~annie.aca.ACA.main_processing` continues: pending commands get executed
       at the current minor cycle (or 1st clock tick), that is *before* the
       :meth:`~annie.aca.ACA.star_data_record_process_per_frame` scheduled at tick number 2,
       and a number of events are added to the ACA and CCD events queues, as illustrated
       in the diagram. These events include :meth:`~annie.aca.ACA.star_data_record_prepare_read`
       which predicts and sets the ``row0, col0`` for the next readout and resets the
       :attr:`~annie.aca.StarDataRecord.repeat_count` counter to zero, and
       :meth:`~annie.aca.CCD.flush` which alters the CCD status from `'idle'` to `'flush'`.
       Both are scheduled at tick number 2. This is followed by the :meth:`~annie.aca.CCD.integrate`
       event which changes the CCD status to `'integrate'` at tick number 3. Then, approximately
       half way through the last minor frame of the current frame,
       :attr:`~annie.aca.CCD.integ_time` / 2 seconds after integration, stars and background
       are scheduled to be shone on the CCD using an appropriate :meth:`~annie.aca.CCD.shine`
       method. Finally, the :meth:`~annie.aca.CCD.read` method is scheduled
       :attr:`~annie.aca.CCD.integ_time` after the integration (that is towards
       the end of the third minor frame of the next frame).
       The :meth:`~annie.aca.CCD.read` method sets the CCD status back to
       `'idle'` and the main processing ACA cycle repeats.


.. _telem-subsystem:

Telemetry
^^^^^^^^^

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

   * - |telem|
     - **The workflow of tasks executed regularly by the telemetry subsystem is as follows:**
       at the start of each minor frame, following the attitude updates performed by the
       PCAD subsystem (see the :ref:`pcad-subsystem` workflow) the telemetry subsystem
       executes :meth:`~annie.telem.Telem.attitude_processing`. This makes a deep copy of
       the current attitude data stored in the :attr:`~annie.pcad.PCAD.att_record`,
       adds a time stamp, and appends these data to the :attr:`annie.telem.Telem.att_records`
       record.

       In addition, the telemetry subsystem executes :meth:`~annie.telem.Telem.main_processing`
       at the start of each frame. This makes a deep copy of the current star data
       stored in the ACA's :attr:`~annie.aca.ACA.star_data_records`, adds a time stamp,
       and appends these data to the :attr:`annie.telem.Telem.star_data_records` record.
       Similarly, a deep copy of the slot record data stored in PCAD's
       :attr:`annie.pcad.PCAD.slot_records` is copied, time stamped and appended to the
       :attr:`annie.telem.Telem.slot_records` record.

Attitude control law
--------------------

The subsections below summarize the concepts of the commanded,
true, sky and estimated attitudes, as well as the attitude
control law adopted by the simulator.
`Attitude control law <_pdf/attitude-control-law.pdf>`_ is
a pdf document that presents these concepts in more detail.
An illustration of the adopted control law can be found
in `this Jupyter notebook <https://github.com/sot/annie/blob/master/validations/attitude_rate_control.ipynb>`_.

Commanded attitude
^^^^^^^^^^^^^^^^^^

The commanded attitude is either fetched from the archive if
the user provides `'obsid'` while setting up an annie
simulation, or it is passed directly by the user
during the setup together with an astropy Table containing
a custom star catalog (see :ref:`annie-setup`).
It is representative of the center of the dither pattern.
It is stored in :attr:`~annie.pcad.PCAD.att_record` as
:attr:`~annie.pcad.AttitudeRecord.att_cmd`.

True attitude
^^^^^^^^^^^^^

The true attitude is the actual simulated spacecraft attitude
and is used (for example) to compute star positions in the ACA field of view and for
post-facto analysis of attitude errors.  The true attitude is
found by integrating the true rates.
The true rates are computed as a sum of the commanded
dither rates and control rates that are proportional to the
attitude errors, R_control = att_error * :data:`~annie.pcad.RATE_SCALE`.
The adopted control law assumes that a 2 arcsec error
results in a 0.1 arcsec/sec adjustment to the rate.
The control rate is allowed to take values in the range
-:data:`~annie.pcad.RATE_MAX` < R_control < :data:`~annie.pcad.RATE_MAX`.
The true attitude is stored in
:attr:`~annie.pcad.PCAD.att_record` as
:attr:`~annie.pcad.AttitudeRecord.att_true`.

Sky attitude
^^^^^^^^^^^^

The sky attitude is set at the start of a simulation to match
the current commanded attitude (see :meth:`~annie.sky.Sky.update_stars`).
It is stored as :attr:`~annie.sky.Sky.att_sky`.
The sky attitude is used to get stars available in ACA field of view
(:meth:`~annie.sky.Sky.get_stars`, :attr:`~annie.sky.Sky.stars`)
if :attr:`~annie.sky.Sky.stars_source` equals ``'agasc'`` (:ref:`annie-setup`).
If the true attitude is found to differ from the commanded attitude by more
than 0.1 deg in any of the yaw, pitch, roll axes (typically after a new
commanded attitude has been provided, e.g. in the case of simulating
a set of observations), the sky attitude is overwritten with
the commanded attitude and the ACA FOV stars are fetched again.

Estimated attitude
^^^^^^^^^^^^^^^^^^

In the absence of new star data, the estimated attitude is computed
in the same way as the true attitute, by integrating the estimated
rates assumed to be equal to the true rates. However, every frame
with new star data (i.e. frames that start with
:attr:`annie.aca.CCD.status` equal `'idle'` and have
:attr:`annie.aca.ACA.repeat_count` equal 0), the estimated attitude
is found from the guide stars using the fast attitude solution
algorithm. The estimated attitude is stored in
:attr:`~annie.pcad.PCAD.att_record` as
:attr:`~annie.pcad.AttitudeRecord.att_est`. It is used to derive
attitude errors as the deviations between the estimated and commanded
attitudes, given the dither pattern.