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.