Minimal working setup for pyobs inside a Jupyter notebook

Introduction

This is a short introduction to using pyobs inside a Jupyter notebook. If you are already familiar with pyobs, you will find that the only big difference is the setup process and how you interact with and manage your own module. If you are new to pyobs, this notebook should give you a good first introduction for writing code which interacts with pyobs.

Setup

Preparations

The following code (and pyobs in general) depends on the asyncio event loop running. If the following code returns False you will need to upgrade your Jupyter version, as older versions don’t support asyncio.

import asyncio
asyncio.get_event_loop().is_running()

Before we start with the actual setup, we have to redirect the loggin output to stdout, so that is displayed inside the cell output. If you want to disable error/warning messages, you can skip this step.

import logging
import sys
logging.basicConfig(stream=sys.stdout)

Credentials

This is not specific to pyobs itself, but is general advice: NEVER commit credentials to a git repository. In the case of the normal pyobs yaml configuration files pyobs allows other config files config files to be imported, in which the credentials can be stored, while the config file containing the credentials can be added to the .gitignore file. In the case of Jupyter notebooks I recommend using .env files, which can be simmilarly excluded from the repo by adding them to the .gitignore file. This also allow you to share the credentials between multiple notebooks in the same probject.

For this to work, simply add a .env file to your project root and add the following lines:

COMM_JID = "[USERNAME]@iag50srv.astro.physik.uni-goettingen.de"
COMM_PWD = "[PASSWORD]"

You want to replace the username and password with your credentials and if you are not at the IAG, replace the address after the @ with the one of your institute. After adding this, the load_dotenv() below, shoud return True

import os
from dotenv import load_dotenv

load_dotenv()
COMM_JID = os.getenv('COMM_JID')
COMM_PWD = os.getenv('COMM_PWD')

Comm

For our pyobs module to work, it needs a valid comm module to communicate with other modules (telescopes, cameras, etc.). In a production environment, XMPP is used for this communication, so a XmppComm module is created with the credentials, that where previously loaded.

from pyobs.comm.xmpp import XmppComm
comm = XmppComm(jid=COMM_JID, password=COMM_PWD, use_tls=True)

Open Comms

Open a channel, lieutenant.

By opening the comm module, we connect it to the pyobs network, so that we can communicate with the other modules on the network.

await comm.open()

If everything works and if other modules are connected to the network, the following command should return the names of these modules:

comm.clients

Closing Comms

At the end of a session, the comm module should be closed again. This signals to the rest of the network, that the module is not longer available.

await comm.close()

Virtual File System (VFS)

If you want to work with a camera, the module also needs access to the pyobs virtual filesystem. Again if you are not at the IAG, you will need to replace the download address below, with your own address.

from pyobs.vfs import VirtualFileSystem
vfs = VirtualFileSystem(
    roots={
        "cache":
        {
            "class": "pyobs.vfs.HttpFile",
            "download": "https://iag50srv.astro.physik.uni-goettingen.de/pyobs/filecache/"
    }})

Usage

The module can now be used to control other modules on the network. comm.proxy(...) needs the “username” of the module it is proxying (in this case, the name of the telescope or camera), and returns an async context manager rather than a usable object directly — a proxy is always obtained as async with comm.proxy(...) as x:, resolved fresh in whichever cell needs it, rather than held in a variable across cells: the remote module can reconnect between cells, and a proxy resolved just before use is guaranteed to be the current one.

Telescope

from pyobs.interfaces import ITelescope, IPointingRaDec, IPointingAltAz

TELESCOPE_NAME = "telescope"

The telescope’s orientation is published as live state rather than fetched via a method call. wait_for_state returns the last known value immediately if one has already arrived, otherwise it waits (up to a timeout) for the first update.

async with comm.proxy(TELESCOPE_NAME, ITelescope) as telescope:
    radec = await telescope.wait_for_state(IPointingRaDec)
    altaz = await telescope.wait_for_state(IPointingAltAz)

radec, altaz

and to move it in altaz coordinates…

async with comm.proxy(TELESCOPE_NAME, ITelescope) as telescope:
    await telescope.move_altaz(alt=60, az=180)

or radec coordiantes (both in degrees).

async with comm.proxy(TELESCOPE_NAME, ITelescope) as telescope:
    await telescope.move_radec(ra=60, dec=25)

Camera

A camera can be used in the same way as a telescope. Here, we use a module with the “username” "sbig6303e" as the camera. Setting the exposure time and image type, then taking the exposure, all happen against the same resolved proxy, so this all lives in one cell. grab_data returns the path to the image in the virtual filesystem, which is then supplied to the vfs module to retrieve the image.

from pyobs.interfaces import ICamera, IExposureTime, IImageType
from pyobs.utils.enums import ImageType

CAMERA_NAME = "sbig6303e"

async with comm.proxy(CAMERA_NAME, ICamera) as camera:
    if isinstance(camera, IExposureTime):
        await camera.set_exposure_time(2)

    if isinstance(camera, IImageType):
        await camera.set_image_type(ImageType.OBJECT)

    image_name = await camera.grab_data(broadcast=False)

img = await vfs.read_image(image_name)

Now we can look at the header…

img.header

and at the image itself.

import matplotlib.pyplot as plt

plt.imshow(img.data, cmap="gray")
plt.show()

We can also save the image as a file.

img.writeto("image_test.fits")