sunspot 0.71
Simple and light-weight JPL ephemeris parser and tracking engine for astronomy and telescope guidance. Provides simple lists of ephemeris data for any Target Body available through the JPL Horizons App. Also provides real-time tracking of those data for the duration of an ephemeris.

<p align="center">
  <img src="https://github.com/phillipcurtsmith/sunspot/blob/main/sunspot.png?raw=true" width="250">
</p>

Simple and light-weight JPL ephemeris parser and tracking engine for astronomy and telescope guidance. Provides simple lists of ephemeris data for any _Target Body_ available through the [JPL Horizons App](https://ssd.jpl.nasa.gov/horizons/app.html#/). Also provides real-time tracking of those data for the duration of an ephemeris.

Sunspot
-
Sunspot recovers ephemerides using JPL's back-end HTML interface. As a result, no need to load binary SPK files (.bsp). A user must only identify a _Target Body_ and _Observer Quantities_ from the [JPL Horizons App](https://ssd.jpl.nasa.gov/horizons/app.html#/). Any planet, satellite, spacecraft, asteroid, or comet available in the [JPL Horizons App](https://ssd.jpl.nasa.gov/horizons/app.html#/) is available through Sunspot. Sunspot provides simple parsed lists of strings for each quantity, e.g., a list of azimuth and elevation values where a _Target Body_ can be found in the sky. Because all data are computed and furnished from JPL, Sunspot incurs very little computational overhead aside from initial parsing.

In addition to data recovery from JPL, Sunspot provides a client tool for tracking _Observer Quantities_. For situations where a user wishes to update a web server as a function of _Target Body_ status, guide a telescope over time, orchestrate hardware control based upon the sun's position, and the like, Sunspot provides a Tracker class to execute user-defined methods with sub-second precision. A user can elect to execute methods before ("setup"), at ("test"), and/or after ("clean-up") individual ephemeris events, for the duration of that ephemeris.

Create Ephemeris
-
One can create an ephemeris for our Sun with a call to the Ephemeris constructor:
```python
>>> import sunspot
>>> e = sunspot.Ephemeris(  '2023-06-18 19:26:00',          # start time as 'YYYY-MM-DD HH:MM:SS' (24h clock)
                            '2023-06-21 19:28:00',          # stop time as 'YYYY-MM-DD HH:MM:SS' (24h clock)
                            '-71.332597,42.458790,0.041',   # 'longitude, latitude, elevation [kilometers]' for observer
                            '1 minute',                     # 'n t', where 1 <= n <= 90024 and t is a unit of time, e.g., 'minute', 'hour', 'day', 'month', 'year'
                            '10',                           # Target body code for our Sun, see below
                            '4')                            # Observer Quantity for apparent azimuth and elevation, see below
```
In this example, the _Target Body_ azimuth and elevation was requested. Those _Observer Quantities_ can be checked:
```python
>>> titles = e.DATA_TITLES
>>> titles
['Date__(UT)__HR:MN:SS', 'Azimuth_(a-app)', 'Elevation_(a-app)']
```
And one can simply view a list of those data:
```python
>>> azimuth = titles[1]
>>> e.get_ephemeris_data( azimuth )
['252.781222419', '253.019121568', ... , '252.896492970', '253.133817632']
```
For each data point in every list, there is a corresponding time stamp. One can use the convenience method to view the time stamps:
```python
>>> e.dates()
['2023-06-18 19:26:00', '2023-06-18 19:27:00', ... , '2023-06-21 19:27:00', '2023-06-21 19:28:00']
```
Each list is always in chronological order such that the nth ````azimuth```` entry corresponds to the nth ````e.dates()```` entry.

One can also use one list of data to identify value(s) in another. For example, one can find at what time(s) the sun can be found at a specific azimuthal angle:
```python
>>> dates = titles[0]
>>> azimuth = titles[1]
>>> east = '90.128595258' 
>>> e.find_corresponding_data(  dates,      # we wish to find a value from this list
                                azimuth,    # using a value from this list
                                east )      # with this specific 'azimuth' value
'2023-06-21 12:41:00'                             
```
Identify Observer Quantities and Target Bodies
-
Sunspot acquires and parses data from the [JPL Horizons App](https://ssd.jpl.nasa.gov/horizons/app.html#/). As such, _Target Bodies_ and _Observer Quantities_ available through the web app are also valid arguments for the Sunspot Ephemeris. However, there are a few things to keep in mind.

- In general, Sunspot will throw errors and print something descriptive if a user attempts to pass an invalid parameter. 
- The surest way to identify a _Target Body_ is to first generate that ephemeris in the [JPL Horizons App](https://ssd.jpl.nasa.gov/horizons/app.html#/). To do this, select _Edit_ at the _Target Body_ row item (row item 2). Use the search function to find your target. Then, _Generate Ephemeris_. The numerical code that prints at the top of the ephemeris can be passed to Sunspot to achieve the same result in python. For example, searching "sun" returns '**Sun [Sol]'**. However, the correct Sunspot argument is **'10'**, which can only be seen at the top of the generated ephemeris. 
- Similarly, _Observer Quantities_ are best identified using the [JPL Horizons App](https://ssd.jpl.nasa.gov/horizons/app.html#/). Select _Edit_ at the _Table Settings_ row item (row item 5). Any combination of _Observer Quantities_ can be requested from a Sunspot Ephemeris by passing the integer value as a comma-deliminated string. In the example above, the Ephemeris ***e*** uses _Observer Quantity_ = '4' to recover target apparent azimuth and elevation. To recover the first five _Observer Quantities_ listed in the App, use '1,2,3,4,5', and so on.

Track Ephemeris data over time
-

Sunspot includes a Tracker class to allow a user to execute a function before, at, or immediately after an Ephemeris event with sub-second precision.

Tracker accepts **user-defined** functions like the following:
```python
def before( args: list ):
    # method executes before Ephemeris entry, 
    # e.g., do 'setup' stuff here
    ...

def on_time( args: list ):
    # method executes at instant of Ephemeris entry, 
    # e.g., do data capture or event confirmation exactly at the moment of an Ephemeris time stamp
    ...

def after( args: list ):
    # method executes after Ephemeris entry, 
    # e.g., do 'clean-up' stuff here
    ...
```
User-defined functions **must** accept a list of strings. This list corresponds to the _Observer Quantities_ for a given Ephemeris entry. For the example Ephemeris above, Tracker methods will be called with a list of three strings: ```'Date__(UT)__HR:MN:SS'```,  ```'Azimuth_(a-app)'```, and ```'Elevation_(a-app)'```. The order of the arguments in the list are the same order as ````e.DATA_TITLES````.

Tracker has a simple constructor to begin tracking:

```python
>>> t = sunspot.Tracker(    e,                                  # an Ephemeris object
                            track_before_method = before,       # user-generated method runs BEFORE Ephemeris event
                            track_on_time_method = on_time,     # user-generated method runs AT Ephemeris event
                            track_after_method = after )        # user-generated method runs AFTER Ephemeris event
```

User-defined functions can have any name. A user can choose to implement one, two, or all three functions.

API Reference
-
**sunspot.Ephemeris**

```python
Ephemeris(  start_time: str, 
            stop_time: str, 
            observer_location: str, 
            step_size: str, 
            target_body: str, 
            quantities: str = '1,2,4' ):
'''
:param start_time: 'YYYY-MM-DD HH:MM:SS'
:param stop_time: 'YYYY-MM-DD HH:MM:SS'
:param observer_location: '00,00,00' as 'latitude [fractional degrees], longitude [fractional degrees], elevation [kilometers]'
:param step_size: 'n t', where 1 <= n <= 90024 (the maximum number of entries) and t is a unit of time, e.g., 'minute', 'hour', 'day', 'month', 'year'
:param target_body: observable target from JPL index, here: https://ssd.jpl.nasa.gov/horizons/app.html#/
:param quantities: observer quantities from JPL index, here: https://ssd.jpl.nasa.gov/horizons/app.html#/ . Default includes right ascension, declination, and altitude/azimuth
'''
```

```python
Ephemeris.dates( )
'''
:return: A list of ephemeris dates, in chronological order.
'''
```

```python
Ephemeris.find_corresponding_data(     target_data_column_title: str, 
                                        source_data_column_title: str, 
                                        source_data_point: str ):
'''
Retrieve data point from within target_data_column, corresponding to source_data_point from within source_data_column.
:param target_data_column_title: String title corresponding to a column of ephemeris data in which to search, e.g., "Azi____(a-app)___Elev"
:param source_data_column_title: String title corresponding to a column of ephemeris data from where search datum originates, e.g., "Date__(UT)__HR:MN:SS"
:param source_data_point: String datum found within source_data_column for which a corresponding row returns.
:return: None if source_data_point not found in source_data. If source_data_point appears only once, return corresponding datum from target_data. If source_data_point appears more than once, return a list of corresponding data in chronological order.
'''
```

```python
Ephemeris.get_ephemeris_data( column_title: str )
'''
:param column_title: String title corresponding to a column of ephemeris data, e.g., "Date__(UT)__HR:MN:SS" or Ephemeris.DATA_TITLES[n] where n is a valid index.
:return: A list of data corresponding to an ephemeris data column title. Entries in this list are in chronological order.
'''
```

```Ephemeris.DATA_TITLES -> list``` : List of column headers associated with _Observer Quantities_ requested by user.

```Ephemeris.RAW_DATA -> str``` : Unparsed data output from JPL.

**sunspot.Tracker**

```python
Tracker(    e: Ephemeris,
            track_before_method: callable( list ) = None,
            track_on_time_method: callable( list ) = None,
            track_after_method: callable( list ) = None,
            verbose: bool = False )
'''
Create Tracker object. Tracking begins automatically upon object creation. Tracker objects will automatically track beginning with the next-soonest date. If no next-soonest date, e.g., all dates are in past, SystemError results.
:param e: Ephemeris object.
:param track_before_method: User-defined method. Must accept list of strings corresponding to Ephemeris _Observer Quantities_. Optional argument.
:param track_on_time_method: User-defined method. Must accept list of strings corresponding to Ephemeris _Observer Quantities_. Optional argument.
:param track_after_method: User-defined method. Must accept list of strings corresponding to Ephemeris _Observer Quantities_. Optional argument.
:param verbose: If True, prints method execution time stamps to terminal.
'''
```
```python
Tracker.terminate_tracking( )
'''
Terminate tracking for a current Tracker object.
:return: None
'''
```