Submitting Observations to the LCO Network

Introduction

Most astronomers are used to taking observations by describing the exact instrument configuration and telescope pointing that they require, perhaps to a Telescope Operator, or via a Phase 2 system. At LCO, our system allows you to describe the observation you need in the same way, and to submit that request directly.

You can do this manually, using our online portal, but its easy to write software to submit it for you and this interface offers a range of extra options which are not available through the web-form.

This notebook is designed to show you how to use this Application Programmable Interface (API) to LCO, though an example of a typical observation.

Example Science Case

Kelt 2b RA = 06:10:39.35 Dec = 30:57:25.7 V = 8.713mag

Our target is a transiting exoplanet with a bright host star. We're going to ask the LCO network to take a series of images of this target as it goes into and out of transit. Our science goals require high precision photometry, so we want to make the exposures longer than ~30s to mitigate the effects of scintillation noise. This is likely to cause the target to saturate in the images, so like many transit observers, we'd like to deliberately defocus the telescope.

LCO telescopes can operate defocused but the option isn't available on the online form - we'll have to use the API.

Describing the observation

We're going to write a short Python script to submit this request for observations to the network, but you can write similar code in whatever language you prefer.

We'll communicate with the network via HTTP, and Python provides a handy library to make this easy:

In [2]:
import requests

Now we'll describe the observation we want to make as a series of python dictionaries.

Firstly, let's provide the details of the target, making sure to convert the coordinates to decimal degrees:

In [3]:
target = {
    'name': 'Kelt2b',
    'type': 'SIDEREAL',
    'ra': 92.66395833333334,
    'dec': 30.95713888888889,
    'epoch': 2000
}

Next we describe the exposures that we'd like to take.

We want to use a 1m telescope in the network to image this object repeatedly over a specific 4hr window timed to coincide with the transit. Including the instrument overheads which you can find here that's 200x30s exposures.

All the LCO 1m have Sinistro imagers, which are as identical as possible, so that's the instrument that we request.

This is also where we can use the extra option to offset the telescope focus during the exposure. 'Defocus' is an optional parameter here, and is zero by default. You can find documentation on the other options available here.

In [4]:
exposures = [
    {
        'type': 'EXPOSE',
        'instrument_name': '1M0-SCICAM-SINISTRO',
        'filter': 'rp',
        'exposure_time': 30,
        'exposure_count': 200,
        'defocus': 0.2
    }
]

Notice that the exposures are specified as a list of dictionaries. This is so you can take multi-filter sequences if you like. For example, the code below would take 2 exposures in SDSS-g, followed by 2 in SDSS-r, followed by 2 in SDSS-i, just by adding dictionaries to the list:

In [5]:
multi_filter_exps = [
    {
        'type': 'EXPOSE',
        'instrument_name': '1M0-SCICAM-SINISTRO',
        'filter': 'gp',
        'exposure_time': 30,
        'exposure_count': 2,
        'defocus': 0.2
    },
    {
        'type': 'EXPOSE',
        'instrument_name': '1M0-SCICAM-SINISTRO',
        'filter': 'rp',
        'exposure_time': 30,
        'exposure_count': 2,
        'defocus': 0.2
    },
    {
        'type': 'EXPOSE',
        'instrument_name': '1M0-SCICAM-SINISTRO',
        'filter': 'ip',
        'exposure_time': 30,
        'exposure_count': 2,
        'defocus': 0.2
    }
]

The next step is to say exactly when we'd like this observation to start and finish.

In [6]:
windows = [{
    'start': '2017-05-02 01:00:00',
    'end': '2017-09-02 05:00:00'
}]

You'll notice that windows is also a list of dictionaries. This means you could request a series of windows if you wanted to. The network would try to schedule the exposures described during each window in the list.

We also need to describe where in the network we want our observations done. In general, its best just to indicate which aperture-class of telescope you want - this tells the scheduler that it can assign this observation to any 1m telescope in the network which is capable of performing the observation.

The scheduler knows, in real-time, which telescopes are available, which are offline for engineering work, what the weather is at each telescope site, and where it will be dark during the time windows where you requested observations. When you submit an observation, the scheduler works out the target visibility, and figures out which telescope sites can observe your target, within those windows.

Of course, conditions at all the telescope sites can change. The scheduler monitors this in real-time, and will re-schedule all observations, changing them from one telescope to another or even between sites if necessary. For this reason, its always a good idea to give the scheduler the maximum possible flexibility in where it can carry out your observation.

So we won't specify a location, except to say that we'll use a 1m (rather than an 0.4m or a 2m) telescope.

In [7]:
location = {
    'telescope_class': '1m0'
}

There are two other constraints which we may want to control - the maximum airmass that we will allow the telescope to point to and the minimum separation from the Moon. The network provides sensible defaults, which we're doing to use here:

In [8]:
constraints = {
    'max_airmass': 1.6,
    'min_lunar_distance': 30
}

Of course, before we observe on any telescope, we first need to have an allocation of time! Whenever you are successful in proposing to LCO, you'll be sent a proposal ID code, which represents the pot of time which will be automatically debited when observations are executed. So we'll need to include this in our observation request, to tell the scheduler which allocation to charge:

In [9]:
proposal_id = 'LCO2017-EXP'

Many projects have several targets which have observations ongoing simultaneously. That's fine - the scheduler will try to do them all if it can. But inevitably there are times when it can't, and in those cases its sometimes helpful for projects to indicate which of their own observations are most important to them. This is called the observation's 'intra-proposal priority' and its specified as a floating point number. 'Normal' priority is 1.0, and you can read here about making use of this feature.

In [10]:
ipp = 1.0

One last thing - its helpful to have a meaningful name for this observation so we can recognize it when we watch its progress through the LCO observe portal.

In [11]:
group_id = 'Kelt-2b_transit_1'

Now, let's put all that together as a complete observation request.

In [12]:
user_request = {
    'group_id': group_id, 
    'proposal': proposal_id,
    'ipp_value': ipp,
    'operator': 'SINGLE',
    'observation_type': 'NORMAL',
    'requests': [{
        'target': target,
        'molecules': exposures,
        'windows': windows,
        'location': location,
        'constraints': constraints
    }]
}

Notice that the 'requests' parameter in the obs_request dictionary is actually a list. If you wanted, you can describe a sequence of different observations under the same group_id. This request is composed of the dictionaries that we specified earlier.

Sending the request to the network

We're ready to submit our observation request to the network.

Since this is done over the wild west of the internet, to protect LCO from hostile attack, we need to establish some credentials.

The good news is that you only need to do this once! The network issues you a token which you can use from then onwards. To get your token, go to your profile page. But treat it like a password and don't share it! If you think its been compromise, you can revoke it from your profile page and get a new one.

A token looks like this:

In [13]:
token = "4310c35ecf9cbde6d1cc3a695fa9914fc91fc64d"

And now we submit our observation as an HTTP request:

In [14]:
response = requests.post(
    'https://observe.lco.global/api/userrequests/',
    headers={'Authorization': 'Token {}'.format(token)},
    json=user_request  
)

Its important to make sure that the user_request dictionary is submitted in JSON format.
The network will respond to tell you if the submission was successful, and will let you know if there's a problem with your request. So its worth parsing the reply:

In [16]:
try:
    response.raise_for_status()
except requests.exceptions.HTTPError as exc:
    print('Request failed: {}'.format(response.content))
    raise exc

obs_request_json = response.json()
Request failed: {"requests":[{"non_field_errors":["According to the constraints of the request, the target is visible for a maximum of 1.29 min within the time window. This is less than the duration of your request 3.81. Consider expanding the time window or loosening the airmass or lunar separation constraints."]}],"proposal":["Invalid pk \"LCO2017-EXP\" - object does not exist."]}
---------------------------------------------------------------------------
HTTPError                                 Traceback (most recent call last)
<ipython-input-16-f25a073567a7> in <module>()
      3 except requests.exceptions.HTTPError as exc:
      4     print('Request failed: {}'.format(response.content))
----> 5     raise exc
      6 
      7 obs_request_json = response.json()

HTTPError: 400 Client Error: Bad Request for url: https://observe.lco.global/api/userrequests/

The network's reply will include your original request in JSON format, together with information on its status and any errors. If the submission was successful, the reply will include the unique tracking ID assigned to your observation by the network. You can get this ID like this:

In [25]:
track_id = obs_request_json['id']
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-25-26fecf955756> in <module>()
----> 1 track_id = obs_request_json['id']

NameError: name 'obs_request_json' is not defined

To monitor the progress of your observation, you can use the LCO observe portal and click on 'Requests' -> 'Submitted', or go straight to this particular request by replacing [track_id] in this URL: https://observe.lco.global/userrequests/[track_id]/

There are also programmatic ways to ask the network to tell you the status of your observation. You can find out more about these at developers.lco.global