<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE article PUBLIC "-//NLM//DTD Journal Archiving and Interchange DTD v2.3 20070202//EN" "archivearticle.dtd">
<article article-type="methods-article" dtd-version="2.3" xml:lang="EN" xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">
<front>
<journal-meta>
<journal-id journal-id-type="publisher-id">Front. Astron. Space Sci.</journal-id>
<journal-title>Frontiers in Astronomy and Space Sciences</journal-title>
<abbrev-journal-title abbrev-type="pubmed">Front. Astron. Space Sci.</abbrev-journal-title>
<issn pub-type="epub">2296-987X</issn>
<publisher>
<publisher-name>Frontiers Media S.A.</publisher-name>
</publisher>
</journal-meta>
<article-meta>
<article-id pub-id-type="publisher-id">891486</article-id>
<article-id pub-id-type="doi">10.3389/fspas.2022.891486</article-id>
<article-categories>
<subj-group subj-group-type="heading">
<subject>Astronomy and Space Sciences</subject>
<subj-group>
<subject>Methods</subject>
</subj-group>
</subj-group>
</article-categories>
<title-group>
<article-title>pyobs - An Observatory Control System for Robotic Telescopes</article-title>
<alt-title alt-title-type="left-running-head">Husser et al.</alt-title>
<alt-title alt-title-type="right-running-head">pyobs</alt-title>
</title-group>
<contrib-group>
<contrib contrib-type="author" corresp="yes">
<name>
<surname>Husser</surname>
<given-names>Tim-Oliver</given-names>
</name>
<xref ref-type="aff" rid="aff1">
<sup>1</sup>
</xref>
<xref ref-type="corresp" rid="c001">&#x2a;</xref>
<uri xlink:href="https://loop.frontiersin.org/people/1631583/overview"/>
</contrib>
<contrib contrib-type="author">
<name>
<surname>Hessman</surname>
<given-names>Frederic V.</given-names>
</name>
<xref ref-type="aff" rid="aff1">
<sup>1</sup>
</xref>
<uri xlink:href="https://loop.frontiersin.org/people/1512979/overview"/>
</contrib>
<contrib contrib-type="author">
<name>
<surname>Martens</surname>
<given-names>Sven</given-names>
</name>
<xref ref-type="aff" rid="aff1">
<sup>1</sup>
</xref>
</contrib>
<contrib contrib-type="author">
<name>
<surname>Masur</surname>
<given-names>Tilman</given-names>
</name>
<xref ref-type="aff" rid="aff1">
<sup>1</sup>
</xref>
<xref ref-type="aff" rid="aff2">
<sup>2</sup>
</xref>
</contrib>
<contrib contrib-type="author">
<name>
<surname>Royen</surname>
<given-names>Karl</given-names>
</name>
<xref ref-type="aff" rid="aff1">
<sup>1</sup>
</xref>
</contrib>
<contrib contrib-type="author">
<name>
<surname>Sch&#xe4;fer</surname>
<given-names>Sebastian</given-names>
</name>
<xref ref-type="aff" rid="aff1">
<sup>1</sup>
</xref>
</contrib>
</contrib-group>
<aff id="aff1">
<sup>1</sup>
<institution>Institute for Astrophysics and Geophysics</institution>, <institution>G&#xf6;ttingen University</institution>, <addr-line>G&#xf6;ttingen</addr-line>, <country>Germany</country>
</aff>
<aff id="aff2">
<sup>2</sup>
<institution>TNG Technology Consulting GmbH</institution>, <addr-line>Unterf&#xf6;hring</addr-line>, <country>Germany</country>
</aff>
<author-notes>
<fn fn-type="edited-by">
<p>
<bold>Edited by:</bold> <ext-link ext-link-type="uri" xlink:href="https://loop.frontiersin.org/people/889972/overview">Aaron Golden</ext-link>, National University of Ireland Galway, Ireland</p>
</fn>
<fn fn-type="edited-by">
<p>
<bold>Reviewed by:</bold> <ext-link ext-link-type="uri" xlink:href="https://loop.frontiersin.org/people/1727458/overview">Ronan Cunniffe</ext-link>, Institute of Physics (ASCR), Czechia</p>
<p>
<ext-link ext-link-type="uri" xlink:href="https://loop.frontiersin.org/people/908999/overview">Jean Baptiste Marquette</ext-link>, UMR5804 Laboratoire d&#x2019;Astrophysique de Bordeaux (LAB), France</p>
<p>
<ext-link ext-link-type="uri" xlink:href="https://loop.frontiersin.org/people/1730978/overview">Martin Dyer</ext-link>, The University of Sheffield, United Kingdom</p>
</fn>
<corresp id="c001">&#x2a;Correspondence: Tim-Oliver Husser, <email>thusser@uni-goettingen.de</email>
</corresp>
<fn fn-type="other">
<p>This article was submitted to Astronomical Instrumentation, a section of the journal Frontiers in Astronomy and Space Sciences</p>
</fn>
</author-notes>
<pub-date pub-type="epub">
<day>11</day>
<month>07</month>
<year>2022</year>
</pub-date>
<pub-date pub-type="collection">
<year>2022</year>
</pub-date>
<volume>9</volume>
<elocation-id>891486</elocation-id>
<history>
<date date-type="received">
<day>07</day>
<month>03</month>
<year>2022</year>
</date>
<date date-type="accepted">
<day>23</day>
<month>05</month>
<year>2022</year>
</date>
<date date-type="publishedonline">
<day>23</day>
<month>05</month>
<year>2022</year>
</date>
</history>
<permissions>
<copyright-statement>Copyright &#xa9; 2022 Husser, Hessman, Martens, Masur, Royen and Sch&#xe4;fer.</copyright-statement>
<copyright-year>2022</copyright-year>
<copyright-holder>Husser, Hessman, Martens, Masur, Royen and Sch&#xe4;fer</copyright-holder>
<license xlink:href="http://creativecommons.org/licenses/by/4.0/">
<p>This is an open-access article distributed under the terms of the Creative Commons Attribution License (CC BY). The use, distribution or reproduction in other forums is permitted, provided the original author(s) and the copyright owner(s) are credited and that the original publication in this journal is cited, in accordance with accepted academic practice. No use, distribution or reproduction is permitted which does not comply with these terms.</p>
</license>
</permissions>
<abstract>
<p>We present a Python-based framework for the complete operation of a robotic telescope observatory. It provides out-of-the-box support for many popular camera types while other hardware like telescopes, domes, and weather stations can easily be added via a thin abstraction layer to existing code. Common functionality like focusing, acquisition, auto-guiding, sky-flat acquisition, and pipeline calibration are ready for use. A remote-control interface, a &#x201c;mastermind&#x201d; for truly robotic operations as well as an interface to the Las Cumbres Observatory observation portal is included. The whole system is fully configurable and easily extendable. We are currently running pyobs successfully on three different types of telescopes, of which one is a siderostat for observing the Sun. pyobs uses open standards and open software wherever possible and is itself freely available.</p>
</abstract>
<kwd-group>
<kwd>methods: observational</kwd>
<kwd>telescopes</kwd>
<kwd>techniques: image processing</kwd>
<kwd>techniques: photometric</kwd>
<kwd>techniques: spectroscopic</kwd>
</kwd-group>
</article-meta>
</front>
<body>
<sec id="s1">
<title>1 Introduction</title>
<p>At the turn of the millennium, a major change was starting to take place in observational astronomy: at first unnoticed by most astronomers, many new telescopes were refitted or newly built for remote or even fully autonomous observations. While there were lots of discussions and even a few robotic telescopes in the early 90&#xa0;s (see, e.g., <xref ref-type="bibr" rid="B33">Perlmutter et al., 1992</xref>; <xref ref-type="bibr" rid="B4">Alcock et al., 1992</xref>), their number grew significantly in the decades thereafter.</p>
<p>While most of the very early robotic telescopes simply monitored known variable stars (e.g., <xref ref-type="bibr" rid="B20">Henry et al., 1995</xref>; <xref ref-type="bibr" rid="B38">Strassmeier et al., 1997</xref>), those that followed were designed to permit very rapid follow-up of gamma-ray bursts&#x2013;e.g., ROTSE-III (<xref ref-type="bibr" rid="B3">Akerlof et al., 2003</xref>), REM (<xref ref-type="bibr" rid="B5">Antonelli et al., 2003</xref>), and BOOTES (<xref ref-type="bibr" rid="B12">Castro-Tirado et al., 2004</xref>)&#x2014;or search for exoplanets (e.g., with SuperWASP, <xref ref-type="bibr" rid="B39">Street et al., 2003</xref>), or to survey galaxies for supernovae (e.g., <xref ref-type="bibr" rid="B15">Filippenko et al., 2001</xref>; <xref ref-type="bibr" rid="B28">Lipunov et al., 2007</xref>). As automation became easier and pipeline software more powerful, it was possible to survey automatically for any transients or moving Solar System objects, e.g., with the Intermediate Palomar Transient Factory (iPTF, <xref ref-type="bibr" rid="B27">Law et al., 2009</xref>), which later was refitted to become the Zwicky Transient Facility (ZTF, <xref ref-type="bibr" rid="B8">Bellm et al., 2019</xref>; <xref ref-type="bibr" rid="B34">Riddle et al., 2018</xref>). About the same time, Las Cumbres Observatory (LCO) started building a whole network of robotic telescopes (<xref ref-type="bibr" rid="B23">Hidas et al., 2008</xref>), now one of the largest in the world.</p>
<p>Robotic telescopes can be used for many things&#x2013;from the automated performance of a heterogeneous list of independent observations to the dedicated performance of a particular scientific project. The unique science that can be done with robotic telescopes almost exclusively concerns transients, i.e., changes over time on the sky of any kind. The ultimate source for such targets in the near future will be the Legacy Survey of Space and Time (LSST) at the Vera C. Rubin Observatory, an 8.4&#xa0;m telescope designed for surveying the sky for any kind of transients (<xref ref-type="bibr" rid="B24">Ivezi&#x107; et al., 2019</xref>). When the LSST starts operating in 2023, a legion of other robotic telescopes will begin doing follow-up observations on the detected transients.</p>
<p>All truly robotic telescopes require a wide palette of hardware&#x2013;e.g., computer-controlled telescopes, cameras, filter wheels, enclosures, and weather stations&#x2013;and software for autonomous operation of the entire system. While this software can be rather basic (to avoid the fully unwarranted word &#x201c;simple&#x201d;) for surveys that just do the same thing over and over again, it gets immensely more complicated for all-purpose telescopes. Unfortunately, this software almost never gets published or placed in a form which is useful for another project, mostly for the reason that it is very specific to the hardware and the science case at hand.</p>
<p>Luckily, there is some software available that tries to be applicable to many different hardware devices and kinds of observations. Especially popular with amateur astronomers is the Windows COM-based ASCOM system<xref ref-type="fn" rid="fn1">
<sup>1</sup>
</xref>, which defines generic interfaces for different kinds of devices and can be used by several client applications. A couple of years ago, a HTTP REST based interface called Alpaca was released, which allows the use of ASCOM in Unix-like systems as well. Additional powerful software like ACP from <ext-link ext-link-type="uri" xlink:href="http://DC3.com">DC3.com</ext-link> can be used to help automate operations within an ASCOM network. A very different system but with the same basic philosophy and breadth of support is the Instrument Neutral Distributed Interface (INDI).<xref ref-type="fn" rid="fn2">
<sup>2</sup>
</xref>, which was designed for network transparency from the beginning and can be used from any system and programmed in any language, although the core libraries are written in C&#x2b;&#x2b;. The 2nd version of the Remote Telescope System (RTS2 for short; <xref ref-type="bibr" rid="B25">Kub&#xe1;nek et al. (2004)</xref>) is widely used in a variety of mostly scientific projects. It provides a complete framework&#x2013;including a back-end database&#x2013;and is designed for fully autonomous operations. RTS2 is written in C&#x2b;&#x2b; and runs on Linux only.</p>
<p>For our own MONET telescopes (<xref ref-type="bibr" rid="B22">Hessman, 2004</xref>, see also <xref ref-type="sec" rid="s6">Section 6</xref>) we first successfully used the robotic control software developed for their twins, the STELLA telescopes (<xref ref-type="bibr" rid="B17">Granzer, 2006</xref>; <xref ref-type="bibr" rid="B18">Granzer et al., 2012</xref>) on Tenerife, operated by the AIP in Potsdam, which was thankfully made available to us by our colleagues there. While the system itself is written in Java, over time we started to implement some functionality using more familiar <italic>Python</italic> scripts. These scripts grew and at some point became pyobs, a fully functional observation control system for robotic telescopes on its own. There was originally no other good reason for developing pyobs than this; without pyobs and starting from scratch, we probably would have chosen INDI. However, pyobs has now grown to a level where it is just as powerful as INDI, RTS2, or ASCOM: it is highly flexible, uses open standards, and is programmed in the language most commonly used by astronomers. Indeed, it stands on the shoulders of giants that are the many amazing open source <italic>Python</italic> projects used in computer science and astronomy. In this paper we will present its architecture and the basic functionality.</p>
<p>We strongly believe in acknowledging the work other people put into publicly available (open-source) software, and thus, references for all the third party software projects used in pyobs are listed in the Acknowledgments. All pyobs packages themselves are published as open-source under the MIT license at GitHub<xref ref-type="fn" rid="fn3">
<sup>3</sup>
</xref>, and its documentation is also available online.<xref ref-type="fn" rid="fn4">
<sup>4</sup>
</xref>
</p>
</sec>
<sec id="s2">
<title>2 Architecture</title>
<p>The astronomical community has spent the last 2 decades migrating from diverse programming languages like IDL, FORTRAN, or C/C&#x2b;&#x2b; to a common denominator, which turned out to be <italic>Python</italic>. As a result, today we have powerful scientific libraries available like NumPy, SciPy, and AstroPy. Following this progress, <italic>Python</italic> was an easy pick as the language of choice for a new Observatory Control System (OCS).</p>
<p>Nevertheless, <italic>Python</italic> has some drawbacks for a large project like this, with the &#x201c;global interpreter lock&#x201d; (GIL) being the most significant. The GIL is a multi-threading lock (or &#x201c;mutex&#x201d;) that can only be acquired by one thread at a time. So, although <italic>Python</italic> supports the creation and running of multiple threads, they never run in parallel. The only way to achieve true parallelism is to use multi-processing, so a decision was made to run pyobs in multiple processes, i.e., one process per block of functionality, which, in pyobs terminology, is called a &#x201c;module&#x201d;. A module can be everything from a controller for an actual hardware device to routines for, e.g., an auto-focus series. With the OCS being split up into multiple processes, the communication between them became one of the most important parts of pyobs.</p>
<sec id="s2-1">
<title>2.1 Asyncio</title>
<p>Given the already mentioned problems with multi-threading in <italic>Python</italic>, it is only logical to rethink the use of threads in pyobs in the first place. Most modules in pyobs do one thing most and foremost: waiting. Waiting for a command to execute, waiting for an exposure to finish, waiting for the dome to move into position. However, in pyobs many things still need to be run concurrently, e.g., a module should still be accepting commands while moving a telescope. Luckily, <italic>Python</italic> introduced a new way of handling concurrency in version 3.5 and improved it steadily in the years thereafter. The new asyncio package uses a main loop and switches between tasks on request, all on a single CPU core and in a single thread. This avoids typical problems in multi-threading like deadlocks and run conditions. However, calling a blocking function in asyncio blocks all other tasks as well, so there is also an easy way for running single methods in an extra thread and waiting for it.</p>
<p>Functions that are running within the asyncio loop are called <italic>coroutines</italic> and are defined with the <monospace>async</monospace> keyword, as will be shown for the interfaces:</p>
<p>
<monospace>class IPointingRaDec(Interface):</monospace>
</p>
<p>
<monospace>&#x2003;async def move_radec(ra: float, dec: float):</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;...</monospace>
</p>
<p>Coroutines can only be called directly from other coroutines and always need to be &#x201c;awaited&#x201d;:</p>
<disp-quote>
<p>
<monospace>async def test():</monospace>
</p>
<p>
<monospace>&#x2003;await telescope. move_radec(1., 2.)</monospace>
</p>
</disp-quote>
<p>They can also be called without actually waiting for them to finish. In those cases, a task should be created which can be awaited later:</p>
<p>
<monospace>task &#x3d; asyncio. create_task(telescope.move_radec(1., 2.))</monospace>
</p>
<p>
<monospace>...</monospace>
</p>
<p>
<monospace>await task</monospace>
</p>
<p>In pyobs, this is, for instance, used for requesting FITS headers from other modules before an exposure is started. The module creates tasks for requesting the headers, but only awaits them after the image has finished, in order not to delay the start of the exposure.</p>
<p>As mentioned above, asyncio heavily reduces the risk of multi-threading related problems. That is, because tasks never run in parallel, but are only switched when one has finished or when something is awaited. In multi-threading, parts of the code that should not be interrupted are often secured using a mutex (or lock), which is mostly unnecessary when using asyncio.</p>
<p>With asyncio, one just needs to be careful with long running functions that are not defined <monospace>async</monospace>, e.g. the readout processes of some cameras. Those method calls would block the whole module, so asyncio provides an easy way to run them in an extra thread:</p>
<disp-quote>
<p>
<monospace>loop &#x3d; asyncio. get_running_loop()</monospace>
</p>
<p>
<monospace>data &#x3d; await loop. run_in_executor(None, camera. read_out())</monospace>
</p>
</disp-quote>
<p>Altogether, pyobs make heavy use of asyncio. For instance, all interface methods and all event handlers must be defined <monospace>async</monospace>. Switching from multi-threading to asyncio massively reduced the number of difficult-to-debug errors and made developing a lot easier.</p>
</sec>
<sec id="s2-2">
<title>2.2 Communication</title>
<p>Instead of inventing our own protocol for communication, we decided to use XMPP (<xref ref-type="bibr" rid="B35">Saint-Andre, 2004</xref>), an XML-based chat protocol. With it being mainly used for instant messaging (e.g., by Jabber, WhatsApp, Zoom, Jitsi, and others), it naturally supports multi-user chat, i.e., sending messages to multiple users. But due to its wide variety of extensions (XMPP Extension Protocol, XEP), it also supports remote procedure calls (RPC, calling methods on another client), and a feature called auto-discovery, which allows one client to determine the capabilities of another.</p>
<p>The use of XMPP also frees us from writing and maintaining our own server software, since there are multiple industrial-grade servers available, like ejabberd<xref ref-type="fn" rid="fn5">
<sup>5</sup>
</xref> and Openfire<xref ref-type="fn" rid="fn6">
<sup>6</sup>
</xref>. They can run with tens of thousands of users, compared to maybe a few dozen pyobs clients in a typical observatory. Although, admittedly, pyobs sends more messages than even the most ambitious teenager in WhatsApp.</p>
<p>While we use the <italic>Python</italic> package Slixmpp for pyobs itself, there are also XMPP libraries available for all major programming languages<xref ref-type="fn" rid="fn7">
<sup>7</sup>
</xref>. Therefore the &#x201c;py&#x201d; (for &#x201c;<italic>Python</italic>&#x201d;) in &#x201c;pyobs&#x201d; refers only to the core package, but extension modules can be written in any language that supports XMPP.</p>
<p>As <xref ref-type="fig" rid="F1">Figure 1</xref> shows, the communication in pyobs is based on three pillars (remote procedure calls, interfaces, events), which all will be discussed in more detail in the following.</p>
<fig id="F1" position="float">
<label>FIGURE 1</label>
<caption>
<p>The three pillars of communication in pyobs, described as interactions between a local module A and a remote module B. On the left, remote procedure calls are actively called on another module. The list of interfaces, in the middle, is automatically retrieved after the connection to the XMPP server has been established. And events can be sent at any time, as shown on the right. Only those events can be handled in a module that it had registered before.</p>
</caption>
<graphic xlink:href="fspas-09-891486-g001.tif"/>
</fig>
<sec id="s2-2-1">
<title>2.2.1 Remote Procedure Calls</title>
<p>All methods within a module that are derived from an interface (see below) can be called remotely. The easiest way to do so, is to get a <monospace>Proxy</monospace> object for another module from pyobs. These objects mimic the behavior of the original module and therefore any of their methods can be called directly as if they were local.</p>
<p>For instance, a camera module might implement this method:</p>
<p>
<monospace>async def set_exposure_time(</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;self,</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;exposure_time: float,</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;&#x2a;&#x2a;kwargs: Any</monospace>
</p>
<p>
<monospace>) -&#x3e; None:</monospace>For another module, calling this method is a simple as:</p>
<p>
<monospace>camera &#x3d; await self. proxy(name_of_camera_module)</monospace>
</p>
<p>
<monospace>await camera. set_exposure_time(2.0)</monospace>
</p>
<p>While some methods should usually return immediately (e.g., requesting a position), some might take a longer time (e.g., exposing an image or moving a telescope). For the caller of a method it would be good to have an estimate for the call duration in order to avoid waiting forever in case of an error. To achieve this, pyobs extends the XEP-0009 extension for RPCs with a timeout mechanism: all methods can define a time after which they should be finished. This time is sent back to the caller immediately after a method is called. If this waiting time is exceeded, a timeout exception is raised and the caller can decide what to do about this. If a method takes longer than 10&#xa0;s, it should be decorated with the <monospace>@timeout</monospace> decorator, which defines the maximum duration:</p>
<p>
<monospace>@timeout(1,200)</monospace>
</p>
<p>
<monospace>async def move_radec(</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;self,</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;ra: float,</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;dec: float,</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;&#x2a;&#x2a;kwargs: Any</monospace>
</p>
<p>
<monospace>) -&#x3e; None:</monospace>
</p>
<p>Calling a method like this works the same way as before, although it now raises an exception only after 1,200 s, compared to 10&#xa0;s for un-decorated methods.</p>
</sec>
<sec id="s2-2-2">
<title>2.2.2 Interfaces</title>
<p>The basis for all RPCs in pyobs are the interfaces in <monospace>pyobs. interfaces</monospace>, which describe methods that a module must implement in order to provide a given functionality. For instance, all telescope modules should implement the <monospace>ITelescope</monospace> interface. While not defining any methods on its own, it inherits the two methods <monospace>move_radec</monospace> and <monospace>get_radec</monospace> from <monospace>IPointingRaDec</monospace> (shortened for clarity):</p>
<p>
<monospace>class IPointingRaDec(Interface):</monospace>
</p>
<p>
<monospace>&#x2003;@abstractmethod</monospace>
</p>
<p>
<monospace>&#x2003;async def move_radec(</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>self,</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;ra: float,</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;dec: float,</monospace>
</p>
<p>
<monospace>&#x2003;) -&#x3e; None:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;...</monospace>
</p>
<p>
<monospace>&#x2003;@abstractmethod</monospace>
</p>
<p>
<monospace>&#x2003;async def get_radec(self, ) -&#x3e; Tuple[float, float]:</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>...</monospace>
</p>
<p>
<monospace>class ITelescope(IPointingRaDec):</monospace>
</p>
<p>&#x2003;<monospace>...</monospace>
</p>
<p>Therefore, in order to be a valid <monospace>ITelescope</monospace>, a module must implement these methods.</p>
<p>All interfaces implemented by a module are published via XMPP&#x2019;s auto-discovery extension, so all other modules can easily determine what functionality is available from a given module. This allows for easy construction of <monospace>Proxy</monospace> objects for RPC. Furthermore, it is extremely simple for a module to find all other modules that implement a given interface. A good example for this are the interfaces <monospace>IFitsHeaderBefore</monospace> and <monospace>IFitsHeaderAfter</monospace>. When a camera starts a new exposure, we usually want to collect FITS headers from different modules. Instead of having this list pre-defined, the camera can just request all modules that implement these interfaces and call their respective methods before and after the exposure:</p>
<p>
<monospace>clients &#x3d; await self.comm.clients_with_interface(</monospace>
</p>
<p>&#x2003;<monospace>IFitsHeaderBefore</monospace>
</p>
<p>
<monospace>)</monospace>
</p>
<p>
<monospace>for client in clients:</monospace>
</p>
<p>
<monospace>&#x2003;proxy &#x3d; await self.proxy(client, &#x2003;IFitsHeaderBefore)</monospace>
</p>
<p>
<monospace>&#x2003;headers[client] &#x3d; &#x2003;await proxy.get_fits_header_before()</monospace>
</p>
<p>This way, we can easily add a new module to the system that simply provides new headers for new FITS files (e.g., with weather data).</p>
<p>As an example, <xref ref-type="fig" rid="F2">Figure 2</xref> shows parts of the inheritance for DummyTelescope, a simulated telescope that can be used for testing.</p>
<fig id="F2" position="float">
<label>FIGURE 2</label>
<caption>
<p>Part of the interface inheritance for <monospace>DummyTelescope</monospace> (on the left in green), a simulated telescope that accepts RA/Dec coordinates and offsets and has a filter wheel, a focus unit, and some temperature sensors. All methods available for remote calls are defined in the interfaces. ITelescope does not define any method of its own, but is just a collection of other interfaces and can be used as a device definition, i.e., &#x201c;this is a telescope&#x201d;.</p>
</caption>
<graphic xlink:href="fspas-09-891486-g002.tif"/>
</fig>
</sec>
<sec id="s2-2-3">
<title>2.2.3 Events</title>
<p>While RPC is an active process of communicating with other modules, there is also a passive one, which is reacting to events. Each module can define types of events that itself creates and that it wants to receive from other modules.</p>
<p>For instance, a camera might want to declare that it can send events, when a new image has been taken</p>
<p>
<monospace>await self.comm.register_event(</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>NewImageEvent</monospace>
</p>
<p>
<monospace>)</monospace>
</p>
<p>and can actually send those events:</p>
<p>
<monospace>await self.comm.send_event(</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>NewImageEvent(filename, image_type)</monospace>
</p>
<p>
<monospace>)</monospace>
</p>
<p>while another module might want to receive those events and handle them in a callback method:</p>
<p>
<monospace>await self.comm.register_event(</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>NewImageEvent,</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>self.on_new_image</monospace>
</p>
<p>
<monospace>)</monospace>
</p>
<p>
<monospace>[...]</monospace>
</p>
<p>
<monospace>async def on_new_image(</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>self,</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>event: Event,</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>sender: str</monospace>
</p>
<p>
<monospace>) -&#x3e; bool:</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>print(event)</monospace>
</p>
<p>The events can be chained by sending new events within a handler method. As an example, events on new images from a camera could be handled by an image pipeline, which in turn sends events that are handled by a module that measures seeing on the reduced images.</p>
</sec>
</sec>
<sec id="s2-3">
<title>2.3 Configuration</title>
<p>Pyobs gets its high flexibility from configuration files in YAML format. The most simple configuration consists of only a single line like:</p>
<p>
<monospace>class: pyobs.modules.test.StandAlone</monospace>
</p>
<p>When running this configuration via <monospace>pyobs config. yaml</monospace> from the command line, a new module is created from the given class and started. The class to use is given by its full package name, the same as one would use to import it in a <italic>Python</italic> shell. Therefore, its definition could be anywhere within the <italic>Python</italic> path and not just in the pyobs package.</p>
<p>The example in the documentation is a little longer:</p>
<p>
<monospace>class: pyobs.modules.test.StandAlone</monospace>
</p>
<p>
<monospace>message: Hello world</monospace>
</p>
<p>
<monospace>interval: 10</monospace>
</p>
<p>Comparing this with the signature of the constructor of the given class:</p>
<p>
<monospace>class StandAlone(Module):</monospace>
</p>
<p>
<monospace>def __init__(</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>self,</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;message: str &#x3d; &#x201c;Hello world&#x201d;,</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;interval: int &#x3d; 10,</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;&#x2a;&#x2a;kwargs: Any</monospace>
</p>
<p>
<monospace>):</monospace>
</p>
<p>This makes it clear that all items in the configuration are simply forwarded directly to the constructor of the given class. pyobs goes even a step further and allows many parameters to be either an object or a configuration dictionary (mostly given in a YAML file as in the example above), describing an object of the same type. For instance, every module class has also a parameter comm (derived from pyobs&#x2019; <monospace>Object</monospace> class) for defining its method for communication with other modules, given as this:</p>
<p>
<monospace>comm: Optional[Union[Comm, Dict[str, Any]]] &#x3d; None</monospace>
</p>
<p>So this parameter accepts both a Comm object directly or a description thereof. A valid configuration file could therefore look like this:</p>
<p>
<monospace>class: pyobs.modules.test.StandAlone</monospace>
</p>
<p>
<monospace>comm:</monospace>
</p>
<p>
<monospace>&#x2003;class: pyobs.comm.slixmpp.XmppComm</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>jid: <email>test@example.com</email>
</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;password: topsecret</monospace>
</p>
<p>Looking at the constructor of given class <monospace>XmppComm</monospace> explains the given parameters (shortened for clarity):</p>
<p>
<monospace>class XmppComm(Comm):</monospace>
</p>
<p>&#x2003;<monospace>def __init__(</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;self,</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>jid: Optional[str] &#x3d; None,</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>password: str &#x3d; "",</monospace>
</p>
<p>
<monospace>&#x2003;):</monospace>
</p>
<p>Note that this makes it possible to replace the whole communication system via XMPP with another method by just implementing a new class derived from <monospace>Comm.</monospace> In an environment, in which it is impossible to run an XMPP server, this could simply be replaced by, e.g., direct socket communication or HTTP REST.</p>
<p>A similar configuration style is used for names of remote modules, which are called within a module. Here is the constructor of the default class for taking an auto-focus series (shortened):</p>
<p>
<monospace>class AutoFocusSeries(Module, IAutoFocus):</monospace>
</p>
<p>&#x2003;<monospace>def __init__(</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;self,</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;focuser: Union[str, IFocuser],</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;camera: Union[str, IImageGrabber],</monospace>
</p>
<p>
<monospace>&#x2003;):</monospace>
</p>
<p>The class needs two remote modules to work, a camera for taking the images and a focus unit with which it can change the actual focus value. Both are defined to accept either a string or an object implementing the interface that is actually required. While for testing, it might be easier to pass an actual object, at the observatory we usually just set the name of the other module. From this name, a proxy object is being created, which is checked for implementing the given interface. Therefore, in production, a configuration for a focus series might look like this:</p>
<p>
<monospace>class: pyobs.modules.focus.AutoFocusSeries</monospace>
</p>
<p>
<monospace>camera: fli230</monospace>
</p>
<p>
<monospace>focuser: telescope</monospace>
</p>
<p>Note that all this behavior is completely up to the class that you want to use. So it must implement the flexibility to accept both an object and a description or a remote name. This should be the case for all modules from the core package and the additional packages.</p>
<p>The default is to have one YAML configuration file per module, but pyobs also has a built-in MultiModule, which can run multiple modules in a single process. This is especially helpful in cases when multiple modules need access to the same hardware, which can be implemented using an object that is shared between those modules. A basic example for this is given with <monospace>DummyTelescope</monospace> and <monospace>DummyCamera</monospace>, which can share a common world simulation, so that the camera actually can simulate images at the position the telescope is pointing to. However, if not necessary, <monospace>MultiModule</monospace> should be avoided in favor of a single module per configuration.</p>
<p>If possible, the configuration even allows changing the core behavior of a module. Coming back to the <monospace>AutoFocusSeries</monospace> class from above, this class itself only defines the functionality for taking a series of images at different focus values. The actual analysis of the images and the calculation of the final best focus is delegated to an object of type <monospace>FocusSeries</monospace> as defined as a parameter in the constructor:</p>
<p>
<monospace>series: Union[Dict[str, Any], FocusSeries]</monospace>
</p>
<p>The default implementation in pyobs (<monospace>ProjectionFocusSeries</monospace> in <monospace>utils. focusseries</monospace>, see <xref ref-type="sec" rid="s3-3-1">Section 3.3.1</xref>) collapses the images along their <italic>x</italic> and <italic>y</italic> axes, respectively, and calculates moments to get a rough size of the stars. The final best focus is calculated using a hyperbola fit to the series of focus and size data. But, given that this class is explicitly specified in the configuration file, it can easily be changed to another (custom) implementation that derives from <monospace>FocusSeries</monospace>.</p>
<p>A module might want to make some configuration settings changeable during runtime. This can be handled via the <monospace>IConfig</monospace> interface, which is implemented by default by all modules and calls internal methods of the form <monospace>_set_config_&#x3c;name&#x3e;</monospace> (if exists) for changing the given variable <monospace>&#x3c;name&#x3e;</monospace>.</p>
</sec>
<sec id="s2-4">
<title>2.4 Virtual File System</title>
<p>In a simple pyobs system, all its modules might run on a single computer. In that case, a module storing a file on a local disk can be certain that another module can access it at the same location. An easy workaround for using this system with modules on different machines is to mount (e.g., via NFS or SMB) the required directories on both machines, but even in that case one has to be careful to mount to the same directory, otherwise filenames would not be the same on both.</p>
<p>This is where a virtual file system (VFS) becomes useful: if we could define a &#x201c;virtual&#x201d; directory that points to the correct location on all computers, the problem would be solved. pyobs provides a VFS in <monospace>pyobs. vfs</monospace> and uses it wherever files are accessed. The VFS is automatically available in all modules, although it needs to be configured. A simple VFS configuration (within the module configuration) might look like this:</p>
<p>
<monospace>vfs:</monospace>
</p>
<p>
<monospace>&#x2003;class: pyobs.vfs.VirtualFileSystem</monospace>
</p>
<p>
<monospace>&#x2003;roots:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;temp:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;&#x2003;class: pyobs.vfs.LocalFile</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;&#x2003;root: /data/images</monospace>
</p>
<p>The VFS in pyobs uses the concept of &#x201c;roots&#x201d; to define where a file is actually located. In this case, one root, <monospace>temp</monospace>, is defined as a <monospace>LocalFile</monospace>, which itself has a <monospace>root</monospace> parameter, pointing to a real directory in the file system&#x2013;note that <monospace>root</monospace> here has nothing to do with the roots system in pyobs&#x2019; VFS, but comes from the term &#x201c;root directory&#x201d;.</p>
<p>Now, within a pyobs module with this configuration we can open a file like this:</p>
<p>
<monospace>fd &#x3d; self.vfs.open_file(&#x201c;/temp/new/image.fits&#x201d;, &#x201c;r&#x201d;)</monospace>
</p>
<p>Internally, pyobs maps the first part of the path (the root), i.e., <monospace>temp</monospace> in this case, to the root of the same name given in the configuration, so it actually creates a <monospace>LocalFile</monospace>. When opening the file, the path is changed accordingly to/<monospace>data/images/new/image.fits</monospace>. Following up on the example from above, now the <monospace>temp</monospace> root can point to different directories on all computers, but still the same filenames can be used on all.</p>
<p>Since the mounting of remote directories might not be possible in some cases, pyobs offers some more classes for file access within the VFS:<list list-type="simple">
<list-item>
<p>&#x2022; <monospace>ArchiveFile</monospace> connects to the pyobs-archive image archive (see <xref ref-type="sec" rid="s4-1">Section 4.1</xref>). Currently only writing is permitted, i.e., uploading an image to the archive.</p>
</list-item>
<list-item>
<p>&#x2022; <monospace>HttpFile</monospace> represents a file on a HTTP server, e.g., the pyobs file cache (see <xref ref-type="sec" rid="s3-4-4">Section 3.4.4</xref>).</p>
</list-item>
<list-item>
<p>&#x2022; <monospace>LocalFile</monospace> is a local file on the machine the module is running on.</p>
</list-item>
<list-item>
<p>&#x2022; <monospace>MemoryFile</monospace> stores a file in memory.</p>
</list-item>
<list-item>
<p>&#x2022; <monospace>SMBFile</monospace> allows access to a file on a Windows share without mounting it.</p>
</list-item>
<list-item>
<p>&#x2022; <monospace>SSHFile</monospace> accesses a file on a remote machine that is accessible via SSH.</p>
</list-item>
<list-item>
<p>&#x2022; <monospace>TempFile</monospace> works on a temporary file that will be deleted after being closed.</p>
</list-item>
</list>
</p>
<p>A file opened via VFS almost works like a normal file-like object in <italic>Python</italic>, with the one difference that all its methods are <monospace>async</monospace>, so they need to be awaited. pyobs also offers some convenience functions for reading and writing FITS, YAML, and CSV files in the VFS.</p>
<p>
<xref ref-type="fig" rid="F3">Figure 3</xref> shows some examples, how a VFS path maps to a real path with a given configuration.</p>
<fig id="F3" position="float">
<label>FIGURE 3</label>
<caption>
<p>Some examples, how a VFS path (left) maps to a real path (right) with a given configuration (middle). Note that for the <monospace>remote</monospace> root the class <monospace>SSHFile</monospace> requires more parameters for the connection, which have been omitted here for clarity.</p>
</caption>
<graphic xlink:href="fspas-09-891486-g003.tif"/>
</fig>
</sec>
<sec id="s2-5">
<title>2.5 Image Processors and Pipelines</title>
<p>With the <monospace>Image</monospace> class in <monospace>pyobs. images</monospace>, pyobs offers a class for reading and writing images that also has support for additional data like a good pixel mask, a star catalog and pixel uncertainties. It is a simple wrapper around the FITS functionality in astropy and is used within pyobs whenever images need to be passed along.</p>
<p>Building on this image class, pyobs has the concept of &#x201c;image processors&#x201d; (defined in <monospace>pyobs</monospace>. <monospace>images.processors</monospace>), which simply take an image, process it in some way, and then return it. Currently, these types of processors are available:<list list-type="simple">
<list-item>
<p>&#x2022; An <bold>astrometry</bold> processor takes an existing catalog attached to the image and tries to plate-solve it (see also <xref ref-type="sec" rid="s4-3">Section 4.3</xref>).</p>
</list-item>
<list-item>
<p>&#x2022; The <bold>detection</bold> processors try to detect objects in the image and write a catalog.</p>
</list-item>
<list-item>
<p>&#x2022; The processors in <bold>exptime</bold> try to estimate a good exposure time from an image, the one existing implementation is for star fields.</p>
</list-item>
<list-item>
<p>&#x2022; In <bold>offsets</bold> are processors that calculate some kind of offsets, usually used for guiding and acquisition.</p>
</list-item>
<list-item>
<p>&#x2022; The <bold>photometry</bold> processors perform photometry on the image (usually at positions determined using a detection step) and write/extend the catalog.</p>
</list-item>
<list-item>
<p>&#x2022; There are some more <bold>misc</bold> processors that can, e.g., add a good pixel mask, calibrate the image, or bin it.</p>
</list-item>
</list>
</p>
<p>Since image processors take an image as input as well as returning one as output, they can easily be chained into an image pipeline. This is done by many modules for pre-processing images in some (fully customizable) way before working on them. Adding new image processors is easily done and provides a perfect way for handling images.</p>
<p>Pyobs also offers a full (offline) image pipeline (in <monospace>utils. pipeline.Night</monospace>) that is also based on image processors, permitting the fully automatic processing of a night&#x2019;s images.</p>
</sec>
<sec id="s2-6">
<title>2.6 Error Handling</title>
<p>Handling errors in a single program is sometimes difficult enough, but it can get rather complicated in a distributed system like pyobs. The basic requirement for every module is that it should handle errors on its own as well as possible (e.g., resolve errors states in hardware devices) but sometimes a calling module needs to be informed about a problem, e.g., if a camera does not respond to requests anymore.</p>
<p>Since error handling can be very specific to the problem at hand, pyobs only provides a framework for dealing with this, not a final solution. It introduces its own set of exceptions that are all derived from <monospace>PyObsError</monospace> in <monospace>pyobs. utils.exceptions</monospace>, and new exceptions can easily be added if required.</p>
<p>A module can call <monospace>register_exception()</monospace>and define a callback that is called whenever a given exception is raised and a given condition is met: the function accepts a limit of how often this can happen (optionally in a given time span) before the problem is escalated. In that case, the raised exception is changed into a <monospace>SevereError</monospace>, keeping the original exception as an attribute. That means, catching one of these severe errors means that an error has occurred too often (in a given time span).</p>
<p>This gets more interesting in a real pyobs system with several modules. There are some cases, in which a module should stop working at all and inform other modules about this. So, e.g., the <monospace>BaseCamera</monospace>, which is the base class for all cameras in pyobs, registers an exception like this:</p>
<p>
<monospace>register_exception(</monospace>
</p>
<p>
<monospace>&#x2003;GrabImageError,</monospace>
</p>
<p>
<monospace>&#x2003;3,</monospace>
</p>
<p>
<monospace>&#x2003;timespan &#x003D; 600,</monospace>
</p>
<p>
<monospace>&#x2003;callback &#x3d; self._default_remote_error_callback</monospace>
</p>
<p>
<monospace>)</monospace>
</p>
<p>This defines that after three occurrences of the <monospace>GrabImageError</monospace> exception within 600&#xa0;s, the given method should be called, which is a default implementation in <monospace>Module</monospace>. It simply logs the error and sets the module to an error state that prevents (almost) any of its methods to be invoked remotely. If another module tries to call methods anyway, it receives a <monospace>ModuleError</monospace>.</p>
<p>If a method is invoked remotely and an exception is raised, this exception is wrapped in a <monospace>RemoteError</monospace> with the original exception stored in an attribute. This is useful to register exceptions with the module parameter, which only registers an exception on a given remote module. For instance, the <monospace>FocusSeries</monospace> module uses this:</p>
<p>
<monospace>register_exception(</monospace>
</p>
<p>
<monospace>&#x2003;RemoteError,</monospace>
</p>
<p>
<monospace>&#x2003;3,</monospace>
</p>
<p>
<monospace>&#x2003;timespan &#x3d; 600,</monospace>
</p>
<p>
<monospace>&#x2003;module &#x3d; camera,</monospace>
</p>
<p>
<monospace>&#x2003;callback &#x3d; self._default_remote_error_callback</monospace>
</p>
<p>
<monospace>)</monospace>
</p>
<p>So, whenever the remote module <monospace>camera</monospace> raises too many exceptions, the <monospace>FocusSeries</monospace> module itself goes into error state, which can be cleared remotely by calling <monospace>reset_error()&#x2014;</monospace> and of course might reappear when the exception is raised again.</p>
<p>Note that registering an exception always also registers parent exceptions. So if exception B is derived from <monospace>A</monospace>, all occurrences of <monospace>B</monospace> also count for the registered limits for <monospace>A</monospace>.</p>
</sec>
</sec>
<sec id="s3">
<title>3 Available Modules</title>
<p>In general there are two types of modules coming with pyobs: those that control actual hardware and those that do not. While the latter are part of the core package, the former are outsourced to separate packages, since they will not be required by everyone and often need special drivers to be installed. All modules can be found on the central GitHub page.</p>
<p>For developing your own modules, please refer to the documentation or just have a look at the existing ones as examples. There is also a simulation available that can be used for first tests. Please see the documentation for details on how to set it up.</p>
<p>
<xref ref-type="table" rid="T1">Table 1</xref> lists all modules available in the core package and in external packages.</p>
<table-wrap id="T1" position="float">
<label>TABLE 1</label>
<caption>
<p>List of available modules in the core package and in external packages.</p>
</caption>
<table>
<thead valign="top">
<tr>
<th colspan="3" align="left">
<bold>Core Package (pyobs.modules.)</bold>
</th>
</tr>
</thead>
<tbody valign="top">
<tr>
<td align="left">
<bold>Module</bold>
</td>
<td align="center">
<bold>Package</bold>
</td>
<td align="center">
<bold>Description</bold>
</td>
</tr>
<tr>
<td align="left">DummyCamera</td>
<td align="left">camera</td>
<td align="left">Dummy camera for testing</td>
</tr>
<tr>
<td align="left">DummySpectrograph</td>
<td align="left">camera</td>
<td align="left">Dummy spectrograph for testing</td>
</tr>
<tr>
<td align="left">FlatField</td>
<td align="left">flatfield</td>
<td align="left">Taking a flat-field series</td>
</tr>
<tr>
<td align="left">FlatFieldPointing</td>
<td align="left">flatfield</td>
<td align="left">Pointing for flat-fields</td>
</tr>
<tr>
<td align="left">FlatFieldScheduler</td>
<td align="left">flatfield</td>
<td align="left">Scheduler for flat-fields</td>
</tr>
<tr>
<td align="left">FocusModel</td>
<td align="left">focus</td>
<td align="left">Temperature model for focus</td>
</tr>
<tr>
<td align="left">FocusSeries</td>
<td align="left">focus</td>
<td align="left">Auto-focus series</td>
</tr>
<tr>
<td align="left">ImageWatcher</td>
<td align="left">image</td>
<td align="left">Watch directory for new images</td>
</tr>
<tr>
<td align="left">ImageWriter</td>
<td align="left">image</td>
<td align="left">Write new images to disk</td>
</tr>
<tr>
<td align="left">Seeing</td>
<td align="left">image</td>
<td align="left">Measure seeing in images</td>
</tr>
<tr>
<td align="left">AutoGuiding</td>
<td align="left">pointing</td>
<td align="left">Auto-guiding with external camera</td>
</tr>
<tr>
<td align="left">Acquisition</td>
<td align="left">pointing</td>
<td align="left">Fine acquisition</td>
</tr>
<tr>
<td align="left">ScienceFrameGuiding</td>
<td align="left">pointing</td>
<td align="left">Auto-guiding with science camera</td>
</tr>
<tr>
<td align="left">DummyAcquisition</td>
<td align="left">pointing</td>
<td align="left">Dummy acquisition for testing</td>
</tr>
<tr>
<td align="left">DummyGuiding</td>
<td align="left">pointing</td>
<td align="left">Dummy guiding for testing</td>
</tr>
<tr>
<td align="left">Mastermind</td>
<td align="left">robotic</td>
<td align="left">Main robotic module</td>
</tr>
<tr>
<td align="left">PointingSeries</td>
<td align="left">robotic</td>
<td align="left">Automated pointing series</td>
</tr>
<tr>
<td align="left">Scheduler</td>
<td align="left">robotic</td>
<td align="left">Task scheduler</td>
</tr>
<tr>
<td align="left">DummyRoof</td>
<td align="left">roof</td>
<td align="left">Dummy roof for testing</td>
</tr>
<tr>
<td align="left">DummyTelescope</td>
<td align="left">telescope</td>
<td align="left">Dummy telescope for testing</td>
</tr>
<tr>
<td align="left">AutonomousWarning</td>
<td align="left">utils</td>
<td align="left">Acoustic warning in robotic mode</td>
</tr>
<tr>
<td align="left">HttpFileCache</td>
<td align="left">utils</td>
<td align="left">File cache</td>
</tr>
<tr>
<td align="left">Kiosk</td>
<td align="left">utils</td>
<td align="left">Take images and publish on website</td>
</tr>
<tr>
<td align="left">Telegram</td>
<td align="left">utils</td>
<td align="left">Telegram interface</td>
</tr>
<tr>
<td align="left">Trigger</td>
<td align="left">utils</td>
<td align="left">Event trigger</td>
</tr>
<tr>
<td align="left">Weather</td>
<td align="left">weather</td>
<td align="left">Connection to pyobs-weather</td>
</tr>
<tr>
<td colspan="3" align="left">
<bold>External packages</bold>
</td>
</tr>
<tr>
<td align="left">
<bold>Module</bold>
</td>
<td align="center">
<bold>Package</bold>
</td>
<td align="center">
<bold>Description</bold>
</td>
</tr>
<tr>
<td align="left">AlpacaTelescope</td>
<td align="left">pyobs_alpaca</td>
<td align="left">Telescope connected via ASCOM Alpaca</td>
</tr>
<tr>
<td align="left">AlpacaFocuser</td>
<td align="left">pyobs_alpaca</td>
<td align="left">Focus unit connected via ASCOM Alpaca</td>
</tr>
<tr>
<td align="left">AlpacaDome</td>
<td align="left">pyobs_alpaca</td>
<td align="left">Dome connected via ASCOM Alpaca</td>
</tr>
<tr>
<td align="left">AravisCamera</td>
<td align="left">pyobs_aravis</td>
<td align="left">Aravis network cameras</td>
</tr>
<tr>
<td align="left">AsiCamera</td>
<td align="left">pyobs_asi</td>
<td align="left">ZWO ASI cameras</td>
</tr>
<tr>
<td align="left">AsiCoolCamera</td>
<td align="left">pyobs_asi</td>
<td align="left">ZWO ASI cameras with active cooling</td>
</tr>
<tr>
<td align="left">FliCamera</td>
<td align="left">pyobs_fli</td>
<td align="left">FLI cameras</td>
</tr>
<tr>
<td align="left">GUI</td>
<td align="left">pyobs_gui</td>
<td align="left">Graphical user interface for remote access</td>
</tr>
<tr>
<td align="left">Pilar</td>
<td align="left">pyobs_pilar</td>
<td align="left">Pilar telescopes</td>
</tr>
<tr>
<td align="left">SbigCamera</td>
<td align="left">pyobs_sbig</td>
<td align="left">SBIG cameras</td>
</tr>
<tr>
<td align="left">SbigFilterCamera</td>
<td align="left">pyobs_sbig</td>
<td align="left">SBIG cameras with filter wheel</td>
</tr>
<tr>
<td align="left">Sbig6303eCamera</td>
<td align="left">pyobs_sbig</td>
<td align="left">SBIG 6303e</td>
</tr>
</tbody>
</table>
</table-wrap>
<sec id="s3-1">
<title>3.1 Cameras</title>
<p>pyobs knows two kinds of cameras: classic cameras (derived from the interface <monospace>ICamera</monospace>), for which one actually starts and stops an exposure, and webcam-like cameras (interface <monospace>IVideo</monospace>), which constantly provide a video (or a series of images) as output. In addition, spectrographs are also supported (interface <monospace>ISpectrograph</monospace>), which output a spectrum instead of an image&#x2013;therefore, most spectrographs would be implemented as a camera, since they return an image, from which the spectrum needs to be extracted.</p>
<p>In the following those camera types are listed, for which stable modules exist and are available via GitHub and PyPi. In addition, we also have modules for Andor and QHYCCD cameras, as well as normal USB webcams (via Video4Linux2), but they are all not in a publishable state. If you need one of those, please contact the author of this paper.</p>
<sec id="s3-1-1">
<title>3.1.1 SBIG</title>
<p>The pyobs-sbig package builds on the SbigDevKit Linux driver for SBIG cameras. It is based on a Cython wrapper around that library&#x2019;s <monospace>CSBIGCam</monospace> and <monospace>CSBIGImg</monospace> classes. The different modules support SBIG cameras with and without filter wheel. There is a additional implementation for the STXL-6303E, due to its different gain at different binnings. Note that this special treatment of single models might be necessary for other cameras. The module has been tested on STXL-6303E, STF-402M, and STF-8300M cameras.</p>
</sec>
<sec id="s3-1-2">
<title>3.1.2 Finger Lakes Instrumentation</title>
<p>A Cython wrapper around the official <monospace>libfli</monospace>
<xref ref-type="fn" rid="fn8">
<sup>8</sup>
</xref> library for FLI cameras is the core of the pyobs-fli. The module has been tested on a FLI ProLine 230.</p>
</sec>
<sec id="s3-1-3">
<title>3.1.3 ZWO ASI</title>
<p>pyobs-asi is a thin wrapper around the zwoasi package to support the cameras by ZWO ASI. It has been tested on a ZWO ASI071MC Pro.</p>
</sec>
<sec id="s3-1-4">
<title>3.1.4 Aravis</title>
<p>Aravis<xref ref-type="fn" rid="fn9">
<sup>9</sup>
</xref> is a library for Genicam cameras connected via gigabit ethernet or USB3. The module in pyobs-aravis uses a modified version of the python-aravis package for communicating with the cameras. It has been tested with several cameras from The Imaging Source<xref ref-type="fn" rid="fn10">
<sup>10</sup>
</xref>.</p>
</sec>
</sec>
<sec id="s3-2">
<title>3.2 Other Hardware</title>
<p>While astronomical cameras are often bought off the shelf and a few brands are most common between observatories, this is mostly quite different for the other hardware in the dome&#x2013;and the dome itself. Those devices are often operated by custom controllers and need special treatment. However, if a driver of any kind exists, it is very simple to write a wrapper for it to be used within a pyobs system.</p>
<p>An attempt to standardize the communication between all kinds of devices has been made with ASCOM. A pyobs module for ASCOM will be described in detail below. Another of those attempts is INDI, for which we do not have a pyobs wrapper yet. Interfaces to those two standards are an easy way to add hardware to a pyobs system, for which ASCOM/INDI drivers already exist.</p>
<sec id="s3-2-1">
<title>3.2.1 ASCOM</title>
<p>ASCOM is a standard for communicating with astronomical devices in Windows and is supported by a wide range of cameras, telescopes, domes, etc. Furthermore, there a many client applications like &#x201c;The Sky&#x201d; or &#x201c;Stellarium&#x201d; that can operate an ASCOM based system.</p>
<p>While pyobs can run on Windows, we made the experience that some things are a little more prone to error on that operating system&#x2013;pyobs processes sometimes quit without warning. There is a (private) pyobs package for calling ASCOM interfaces directly on Windows, but due to these problems, we never published it. However, we can provide access on request.</p>
<p>The restriction to Windows systems is due to the use of Windows COM as means for communication, which is not available for other operating systems. Luckily, in 2018 ASCOM presented a new interface, called Alpaca, which is based on HTTP REST requests, and therefore can also be accessed from Unix-like systems. The pyobs-alpaca package provides modules for telescopes, domes and focus units via Alpaca. However, in contrast to most other modules in the pyobs ecosystem, these ones are not meant to be used directly, but more as some kind of inspiration for an observatory specific implementation. They are not a general implementation of the ASCOM protocol, but tailored specifically for the use case of the 50&#xa0;cm Cassegrain telescope based at the Institute for Astrophysics and Geophysics in G&#xf6;ttingen.</p>
</sec>
<sec id="s3-2-2">
<title>3.2.2 Pilar</title>
<p>Pilar is a telescope control software from &#x201c;4pi Systeme&#x201d;<xref ref-type="fn" rid="fn11">
<sup>11</sup>
</xref> based on the Open Telescope Software Interface (OpenTSI), and currently used by our MONET telescopes via the pyobs-pilar package. While the specific implementation of this module might not be of interest for most observatories, it shows an example for a socket based communication protocol wrapped in a pyobs module.</p>
</sec>
</sec>
<sec id="s3-3">
<title>3.3 Automating</title>
<p>While the modules described so far are all built around a specific piece of hardware, there are also those that purely consist of software to automate the boring stuff.</p>
<sec id="s3-3-1">
<title>3.3.1 Auto-Focus</title>
<p>A common problem in astronomy is focusing the image on the camera sensor. In most cases this will be done by moving either a mirror (mostly the secondary) or the camera back and forth until stars appear sharp, i.e., with the smallest possible width. The <monospace>AutoFocusSeries</monospace> (in <monospace>pyobs. modules.focus</monospace>) module accomplishes this by taking a series of images at different focus values (i.e., position of M2 or camera), and tries to find an optimal focus by fitting a hyperbola through the estimated star widths in each image as a function of focus value. For this, references to a camera and a focus unit must be specified so that they can be controlled remotely.</p>
<p>The estimation of star sizes is fully configurable by injecting a class implementing the <monospace>FocusSeries</monospace> (in <monospace>pyobs. utils.focusseries</monospace>) interface. Our current default implementation is defined in <monospace>ProjectionFocusSeries</monospace>, which projects the image along its <italic>x</italic> and <italic>y</italic> axis, respectively, and measures moments on the resulting 1D data. Another possibility is to use a method for star detection/photometry for estimating star widths, as used in <monospace>PhotometryFocusSeries</monospace>.</p>
<p>While especially smaller telescope will typically work well with a constant focus value throughout the whole night, larger telescopes (with a steel structure) are constantly changing their size (and therefore the position of the perfect focus) due to temperature changes. For these cases, pyobs provides a temperature model for the focus, which is implemented in the <monospace>modules.focus.FocusModel</monospace> module and can adjust the focus continuously throughout the night. The configuration needs to specify a model function like this:</p>
<p>
<monospace>model: &#x2212;0.043&#x2a;T1 - 0.03&#x2a;T2 &#x2b; 0.06&#x2a;temp &#x2b; 41.69</monospace>
</p>
<p>While the value for <monospace>temp</monospace> is automatically fetched from a given weather module (see <xref ref-type="sec" rid="s3-4-1">Section 3.4.1</xref>), those for <monospace>T1</monospace> and <monospace>T2</monospace> must also be specified in the configuration. In this case they are mirror temperatures and are supposed to be requested from the telescope module:</p>
<p>
<monospace>temperatures:</monospace>
</p>
<p>
<monospace>&#x2003;T1:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;module: telescope</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;sensor: T1</monospace>
</p>
<p>
<monospace>&#x2003;T2:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;module: telescope</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;sensor: T2</monospace>
</p>
<p>For this to work, a module named <monospace>telescope</monospace> must exist and its <monospace>get_temperatures</monospace> method must return values for <monospace>T1</monospace> and <monospace>T2</monospace>. With these values the module now calculates new focus values at a given interval and sets them accordingly.</p>
<p>An <monospace>AutoFocusSeries</monospace> also sends an event, when it has successfully determined a new focus, which can be handled by the <monospace>FocusModel</monospace> automatically to optimize its temperature model. For that to work, the model function must be defined with variables that can be fitted:</p>
<p>
<monospace>model: a&#x2a;T1 &#x2b; b&#x2a;T2 &#x2b; c&#x2a;temp &#x2b; d</monospace>
</p>
<p>In this case, a set of default values must also be provided:</p>
<p>
<monospace>coefficients:</monospace>
</p>
<p>
<monospace>&#x2003;a: &#x2212;0.043</monospace>
</p>
<p>
<monospace>&#x2003;b: &#x2212;0.031</monospace>
</p>
<p>
<monospace>&#x2003;c: 0.062</monospace>
</p>
<p>
<monospace>&#x2003;d: 41.694</monospace>
</p>
<p>If this is set up correctly, a fully robotic system can perform multiple focus series during each night (e.g., if there is nothing else to do) and automatically optimize the focus temperature model over time.</p>
</sec>
<sec id="s3-3-2">
<title>3.3.2 Flat-Fielding</title>
<p>A task that is prone to be automated as early as possible is flat-fielding. While this is quite simple in a controlled environment with a closed dome, e.g., with a flat-field screen, it becomes more challenging when done on-sky during twilight. In that case, exposure times have to be adjusted continuously to obtain optimal count rates on the images.</p>
<p>To perform this task in a fully automatic way, it is best to first measure optimal exposure times as a function of solar altitude. For taking flat-fields, we always point the telescope at the same sweet spot on the sky, right opposite the Sun at an altitude of 80&#xb0; (see <xref ref-type="bibr" rid="B13">Chromey and Hasselbacher, 1996</xref>). That way, we get comparable count rates for a given solar altitude and exposure time. We take a series of flat-fields, for which we try to get a constant flux level&#x2013;in our case 30, 000 counts &#x2013;, and calculate the optimal exposure time that would be required to get exactly the given level. <xref ref-type="fig" rid="F4">Figure 4</xref> shows this for a set of RGBC filters and three different binnings as measured at the 50&#xa0;cm Cassegrain telescope based at the Institute for Astrophysics and Geophysics in G&#xf6;ttingen.</p>
<fig id="F4" position="float">
<label>FIGURE 4</label>
<caption>
<p>An example for empirical models for flatfield exposure times. The points are optimal exposure times for getting a mean flux of 30,000 counts in the image as a function of solar altitude. The colors indicate different filters and binnings. A fit with an exponential function was performed and the best coefficients are given in the legend and plotted as lines.</p>
</caption>
<graphic xlink:href="fspas-09-891486-g004.tif"/>
</fig>
<p>As one can see, the measured points do not overlap perfectly over several nights, which can be caused, e.g., by clouds. But the data is good enough to fit exponential functions to it (see lines in plot), which we can use to roughly estimate the optimal exposure time for a given solar altitude.</p>
<p>During dusk twilight, a flat-field module picks a filter and binning combination and estimates the exposure time <italic>t</italic> for the current solar altitude. If the time is shorter than a given minimum <italic>T</italic>
<sub>min</sub>, it does nothing and waits. When <italic>t</italic> reaches 0.5 &#x22c5; <italic>T</italic>
<sub>min</sub>, test exposures are started, actually measuring the counts in the image, and calculate a new best exposure time. Only when <italic>t</italic> &#x2265; <italic>T</italic>
<sub>min</sub> the module starts taking actual flat-fields until either a given number of images has been taken or <italic>t</italic> gets larger than a given maximum <italic>T</italic>
<sub>max</sub>. In dawn twilight, the procedure can be performed accordingly with <italic>T</italic>
<sub>min</sub> and <italic>T</italic>
<sub>max</sub> swapped and opposite comparisons. This is implemented in the <monospace>FlatField</monospace> module in <monospace>modules.flatfield</monospace>.</p>
<p>The class handling the actual flat-fielding is, again, fully configurable. An example for the <monospace>flat_fielder</monospace> parameter of the module might look like this, defining functions for the exposure time for different binnings and filters:</p>
<p>
<monospace>class: pyobs.utils.skyflats.FlatFielder</monospace>
</p>
<p>
<monospace>pointing:</monospace>
</p>
<p>
<monospace>&#x2003;class: pyobs.utils.skyflats.pointing.SkyFlatsStaticPointing</monospace>
</p>
<p>
<monospace>combine_binnings: False</monospace>
</p>
<p>
<monospace>functions:</monospace>
</p>
<p>
<monospace>&#x2003;1 &#xd7; 1:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;Clear: exp(&#x2212;1.22421&#x2a;(h&#x2b;4.06676))</monospace>
</p>
<p>
<monospace>&#x2003;2 &#xd7; 2:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;Clear: exp(&#x2212;0.99118&#x2a;(h&#x2b;4.66784))</monospace>
</p>
<p>
<monospace>&#x2003;3 &#xd7; 3:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;Clear: exp(&#x2212;1.14748&#x2a;(h&#x2b;5.38661))</monospace>
</p>
<p>The given class for <monospace>pointing</monospace> can also be used in the <monospace>FlatFieldPointing</monospace> module, which only points the telescope to a specific position without taking flat-fields. This can be useful, if multiple instruments are supposed to be flat-fielded at the same time.</p>
<p>A twilight is usually long enough for taking flat-fields in more than one filter/binning combination. The <monospace>FlatFieldScheduler</monospace> module provides a way to run multiple ones as long as the twilight lasts. It can also read priorities from a customizable source, which can be, e.g., an image archive, so that the priorities are the larger the longer ago the last flat-fields in this combination were taken. The most simple solution, though, is not covered by this scheduler: just a list of filter/binning combinations, which are flat-fielded in the order as they are given. However, the implementation would be so simple that we can leave it as an exercise to the reader.</p>
</sec>
<sec id="s3-3-3">
<title>3.3.3 Acquisition</title>
<p>After moving a telescope to a target, it is often off by some arcseconds or even arcminutes. Sometimes this is unacceptable, especially when the light of a star, e.g., needs to be coupled into a small fiber. In those cases, a fine acquisition based on images from some camera is required. The <monospace>Acquisition</monospace> module (in <monospace>modules.pointing</monospace>) takes images, runs them through a pipeline (see <xref ref-type="sec" rid="s2-5">Section 2.5</xref>) to determine what offset to move the telescope, and then applies this offset. This is repeated until the offset is smaller than a given limit.</p>
<p>The configuration for the pipeline typically consists of three steps:</p>
<p>
<monospace>pipeline:</monospace>
</p>
<p>&#x2003;<monospace>- class: pyobs.images.processors.detection.SepSourceDetection</monospace>
</p>
<p>
<monospace>&#x2003;- class:pyobs.images.processors.astrometry.AstrometryDotNet</monospace>
</p>
<p>
<monospace>url: <ext-link ext-link-type="uri" xlink:href="https://astrometry.example.com/">https://astrometry.example.com/</ext-link>
</monospace>
</p>
<p>
<monospace>radius: 5</monospace>
</p>
<p>
<monospace>&#x2003;- class: pyobs.images.processors.offsets.AstrometryOffsets</monospace>
</p>
<p>First, a source detection is run on the images, followed by an attempt to plate-solve it using the service of <ext-link ext-link-type="uri" xlink:href="http://Astrometry.net">Astrometry.net</ext-link> (<xref ref-type="bibr" rid="B26">Lang et al., 2010</xref>), for which we provide a self-hosted solution (see <xref ref-type="sec" rid="s4-3">Section 4.3</xref>). In the last step, the found coordinates are compared to those from the pointing, and an offset is calculated. Alternative methods are possible by simply changing the pipeline. For instance, an image processor could find the brightest star in the image and set the offset to move the telescope there.</p>
<p>Applying the offset to the telescope is also fully configurable. For a telescope that accepts RA/Dec offsets, it might look like this:</p>
<p>
<monospace>apply:</monospace>
</p>
<p>
<monospace>&#x2003;class: pyobs.utils.offsets.ApplyRaDecOffsets</monospace>
</p>
<p>
<monospace>&#x2003;max_offset: 3,600</monospace>
</p>
<p>The given class simply takes the offsets from the image (written by an image processor) and moves the telescope accordingly.</p>
</sec>
<sec id="s3-3-4">
<title>3.3.4 Auto-Guiding</title>
<p>The task of auto-guiding is quite similar to that of acquisition, so the configuration is as well: it also mainly consists of a <monospace>pipeline</monospace> and an <monospace>apply</monospace> step. But instead of running until the calculated offset is small enough, the auto-guiding runs forever, or until stopped, to correct for any shift in pointing that the telescope is doing over time.</p>
<p>In pyobs, two kinds of auto-guiding are ready to use:<list list-type="simple">
<list-item>
<p>&#x2022; <bold>Science-frame auto-guiding</bold> (module <monospace>ScienceFrameAutoGuiding</monospace>) uses the images of the science camera for guiding. This works quite well if the exposure time is small enough to correct for any shifts of the telescope over time.</p>
</list-item>
<list-item>
<p>&#x2022; In contrast, what we just call <bold>auto-guiding</bold> (module <monospace>AutoGuiding</monospace>) requires an extra camera that is mounted, e.g., at the same focal plane as the science camera or at an extra guiding telescope that moves along with the main telescope. In this case, with a bright enough star in the field, the auto-guiding can perform its corrections, independently of the actual science taken, in intervals as short as required.</p>
</list-item>
</list>
</p>
<p>While the astrometric method used in the acquisition would also work for auto-guiding, it is usually too slow. These alternative methods are provided with pyobs:<list list-type="simple">
<list-item>
<p>&#x2022; A projection method as implemented by <monospace>ProjectedOffsets</monospace> projects the images separately along <italic>x</italic> and <italic>y</italic> axis and cross-correlates both individually with a reference image. The resulting x/y pixel offset can be translated into a RA/Dec or Alt/Az offset.</p>
</list-item>
<list-item>
<p>&#x2022; Cross-correlating full images is usually too slow, so <monospace>NStarOffsets</monospace> uses star positions from a source detection that needs to run before, and cross-correlates only small images around the <italic>N</italic> brightest stars in the image.</p>
</list-item>
</list>
</p>
</sec>
</sec>
<sec id="s3-4">
<title>3.4 Utilities</title>
<p>A couple of smaller utility modules for common tasks are provided for convenience.</p>
<sec id="s3-4-1">
<title>3.4.1 Weather</title>
<p>For fully autonomous observatories, the most important task is not to get observations done, but to close the roof on bad weather and to keep it closed&#x2013;an expensive telescope and camera is worth nothing if regularly rained on. With pyobs-weather (see <xref ref-type="sec" rid="s4-2">Section 4.2</xref>) there is an affiliated project that acts as an aggregator for data from several weather stations and evaluates some logic to determine, whether the weather is good or bad, i.e., suitable for observations or not.</p>
<p>The <monospace>Weather</monospace> module connects to an instance of pyobs-weather and can provide several functions within a pyobs network:<list list-type="simple">
<list-item>
<p>&#x2022; It provides FITS header entries with weather information for science data.</p>
</list-item>
<list-item>
<p>&#x2022; It has a simple <monospace>is_weather_good()</monospace> method returning a Boolean, indicating whether the weather is good or not.</p>
</list-item>
<list-item>
<p>&#x2022; It sends events when the weather status changes, <monospace>GoodWeatherEvent</monospace> and <monospace>BadWeatherEvent</monospace>, which other modules can handle and react accordingly.</p>
</list-item>
</list>
</p>
<p>Note that the safety net cast by this module is mainly for the robotic system to react on changes. It is not a replacement for an emergency shutdown in case of, e.g., rain, which should work even without network.</p>
</sec>
<sec id="s3-4-2">
<title>3.4.2 Telegram</title>
<p>Even the best logging is only good, if someone reads it. Therefore, the module <monospace>Telegram</monospace> can forward all messages of a given level (info, warning, error, &#x2026; ) into a Telegram<xref ref-type="fn" rid="fn12">
<sup>12</sup>
</xref> chat &#x2013; the default configuration would be to have only error messages sent. That way the telescope administrator usually gets notified of a problem within seconds.</p>
<p>The Telegram bot used for this provides several commands that can be issued to it by simply opening a chat on the smart phone. For security reasons, every user has to login (<monospace>/login</monospace> command) before receiving any logs and before being able to issue any other command./<monospace>loglevel</monospace> changes the current log level and/<monospace>modules</monospace> lists all online modules. The most powerful command is/<monospace>exec</monospace>, which allows the user to issue any pyobs command to any module, similar to what is possible within a module or in the Shell of the GUI (see <xref ref-type="sec" rid="s3-5">Section 3.5</xref>). Using this, the administrator can easily shut the roof or abort an observation from within a Telegram chat.</p>
<p>While Telegram currently is the only supported chat system, adding other ones should be as simple, as long as an API is provided that can be used by pyobs.</p>
</sec>
<sec id="s3-4-3">
<title>3.4.3 Trigger</title>
<p>Events are a powerful system in pyobs and for some of them a default action should be performed every time they are encountered. Instead of writing a new module for this, one can simply use the existing <monospace>Trigger</monospace> module. It defines events and the method on a given module that should be executed, when the event is triggered. For example:</p>
<p>
<monospace>triggers:</monospace>
</p>
<p>
<monospace>&#x2003;- event: pyobs.events.GoodWeatherEvent</monospace>
</p>
<p>
<monospace>module: dome</monospace>
</p>
<p>
<monospace>method: init</monospace>
</p>
<p>&#x2003;<monospace>- event: pyobs.events.RoofOpenedEvent</monospace>
</p>
<p>
<monospace>module: telescope</monospace>
</p>
<p>
<monospace>method: init</monospace>
</p>
<p>This configuration calls <monospace>dome.init()</monospace> on a <monospace>GoodWeatherEvent</monospace> and <monospace>telescope.init()</monospace> on a <monospace>RoofOpenedEvent</monospace>, thus opening roof and telescope when the weather changes from bad to good&#x2013;which, in case of pyobs-weather, is usually also the case after sunset for a night telescope. Note that there is no trigger configuration for the bad weather case, since all modules handle that on their own.</p>
</sec>
<sec id="s3-4-4">
<title>3.4.4 FileCache</title>
<p>While a camera module can be configured to store its files locally, that can be quite impractical, if it runs on a different computer than the rest of the pyobs system, which might be the case quite often. So there is need for a place to store the images that can be accessed from all modules&#x2013;or at least those that need access to the images.</p>
<p>A network mount using, e.g., SMB or NFS does the job well, but with <monospace>HttpFileCache</monospace> there is also a module available for that in pyobs. It opens a web server on a given port, which can be used to upload images from the camera and download them somewhere else. It can simply be accessed via the VFS (see <xref ref-type="sec" rid="s2-4">Section 2.4</xref>) using a <monospace>HttpFile</monospace> root.</p>
</sec>
<sec id="s3-4-5">
<title>3.4.5 ImageWriter and ImageWatcher</title>
<p>When the camera uploads its images to a FileCache (see above), they should still be stored somewhere, since the cache only holds a limited amount of files. An easy way to do that is the <monospace>ImageWriter</monospace> module that waits for <monospace>NewImageEvents</monospace>, downloads those images and stores them at a different VFS location.</p>
<p>To make this a little safer and reduce the risk of losing images, an <monospace>ImageWriter</monospace> should always write images to a local disk. If they are supposed to be copied to a remote location, the preferred way is an additional <monospace>ImageWatcher</monospace>, which watches a given path for new files, copies the files somewhere else, and only deletes the original files if there was no error. So a typical setup would configure the <monospace>ImageWriter</monospace> to store its files into a local directory like this, assuming that the camera stores its images at <monospace>/cache/ and /some/temp/dir/</monospace> is some local temp directory:</p>
<p>
<monospace>class: pyobs.modules.image.ImageWriter</monospace>
</p>
<p>
<monospace>vfs:</monospace>
</p>
<p>
<monospace>&#x2003;class: pyobs.vfs.VirtualFileSystem</monospace>
</p>
<p>
<monospace>&#x2003;roots:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;cache:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;&#x2003;class: pyobs.vfs.HttpFile</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;&#x2003;download: <ext-link ext-link-type="uri" xlink:href="http://somewhere:37075/">http://somewhere:37075/</ext-link>
</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>archive:</monospace>
</p>
<p>&#x2003;&#x2003;&#x2003;<monospace>class: pyobs.vfs.LocalFile</monospace>
</p>
<p>&#x2003;&#x2003;&#x2003;<monospace>root:/some/temp/dir/</monospace>
</p>
<p>Note that the root <monospace>archive</monospace> is used since the default value for the <monospace>filenames</monospace> parameter of the module is/<monospace>archive/FNAME</monospace>.</p>
<p>After the images have been stored locally, an <monospace>ImageWatcher</monospace> should pick them up and copy them into an archive (note that curly brackets in <monospace>destinations</monospace> indicate placeholders which are filled from FITS header values):</p>
<p>
<monospace>class: pyobs_iagvt.filewatcher.FileWatcher</monospace>
</p>
<p>
<monospace>watchpath:/temp/</monospace>
</p>
<p>
<monospace>destinations:</monospace>
</p>
<p>
<monospace>&#x2003;-/archive/{FNAME}</monospace>
</p>
<p>
<monospace>vfs:</monospace>
</p>
<p>
<monospace>&#x2003;class: pyobs.vfs.VirtualFileSystem</monospace>
</p>
<p>
<monospace>&#x2003;roots:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;temp:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;&#x2003;class: pyobs.vfs.LocalFile</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;&#x2003;root:/some/temp/dir/</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;archive:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;&#x2003;class: pyobs.vfs.ArchiveFile</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;&#x2003;url: <ext-link ext-link-type="uri" xlink:href="https://archive.example.com/">https://archive.example.com/</ext-link>
</monospace>
</p>
<p>After the images have been copied into the archive, they will be delete from the temp directory by the <monospace>ImageWatcher</monospace>. Currently the copied files are not validated in order to make sure that they are identical to the original, but this would be a simple feature to add.</p>
</sec>
</sec>
<sec id="s3-5">
<title>3.5 Graphical User Interface</title>
<p>While all the other modules presented here are fully autonomous, pyobs also provides a graphical user interface (GUI) for easy (remote) access to the system. Technically it is also just another module, which opens a window for interaction with the user.</p>
<p>
<xref ref-type="fig" rid="F5">Figure 5</xref> shows a screenshot of the GUI right after a bias image has been taken with the selected SBIG camera. The main window of the GUI consists of three major parts:<list list-type="simple">
<list-item>
<p>&#x2022; The <bold>list of module pages</bold> on the left, including the three special pages Shell, Events, and Status.</p>
</list-item>
<list-item>
<p>&#x2022; The <bold>system log</bold> on the bottom, showing all log entries from all connected modules as well as a list of all those modules on the lower right.</p>
</list-item>
<list-item>
<p>&#x2022; The <bold>module page</bold>, filling the rest of the window, which changes depending on the selected module.</p>
</list-item>
</list>
</p>
<fig id="F5" position="float">
<label>FIGURE 5</label>
<caption>
<p>A screenshot of the graphical user interface (GUI) as provided by pyobs-gui. It shows the list of connected modules that are supported by the GUI on the left. When selecting one, a custom widget for each kind of module is shown in the main area right of it. Below is the logging area, which shows log entries from all connected modules.</p>
</caption>
<graphic xlink:href="fspas-09-891486-g005.tif"/>
</fig>
<p>The three special pages mentioned above are:<list list-type="simple">
<list-item>
<p>&#x2022; The <bold>Shell</bold> is an interactive command prompt, in which the user can execute any command on any module in the form &#x3c;<monospace>module&#x3e;.&#x3c;method&#x3e;(&#x3c;params&#x3e;</monospace>). This makes the shell a very powerful tool for admins and for debugging.</p>
</list-item>
<list-item>
<p>&#x2022; The <bold>Events</bold> page shows a chronological list of all events that have been sent in the pyobs network. It also allows to send events on its own with parameters defined by the user.</p>
</list-item>
<list-item>
<p>&#x2022; The <bold>Status</bold> page shows the current status of a module, e.g., whether it is in an error state. It also shows the pyobs version of every module to keep track of updates.</p>
</list-item>
</list>
</p>
<p>In the list of modules on the left, not all modules are listed, but only those for which a graphical user interface has been designed. The GUI is fully dynamic, which means that it changes according to the list of connected modules. Single module pages also adapt to the capabilities of the associated module, e.g., the camera page only shows options for window and binning, if the camera supports it.</p>
<p>The customization of the GUI goes even further with user-defined pages. For example, pyobs does not provide a user interface for acquisition and guiding, but in the case of our solar telescope, a visual feedback is important. So we created a new widget and defined it in the configuration of the GUI:</p>
<p>
<monospace>widgets:</monospace>
</p>
<p>
<monospace>- module: guiding</monospace>
</p>
<p>
<monospace>overwrite: True</monospace>
</p>
<p>
<monospace>widget:</monospace>
</p>
<p>
<monospace>class: pyobs_iagvt.guidingwidget.GuidingWidget</monospace>
</p>
<p>
<monospace>acquisition: acquisition</monospace>
</p>
<p>This tells the GUI to overwrite an existing widget for the <monospace>guiding</monospace> module with the given class. Using custom widgets, one can adapt the GUI to work with any special requirements.</p>
<p>The other way around, restricting access in the GUI, can also be accomplished in the configuration via the <monospace>show_shell, show_events, and show_status</monospace> parameters, which, if set to False, hide the corresponding page. An explicit list of allowed module pages can be provided with the <monospace>show_modules</monospace> parameter. Here is an example for a very limited access to the camera only:</p>
<p>
<monospace>show_shell: False</monospace>
</p>
<p>
<monospace>show_events: False</monospace>
</p>
<p>
<monospace>show_status: False</monospace>
</p>
<p>
<monospace>show_modules: [camera]</monospace>
</p>
<p>Altogether, the GUI tries to allow access to all modules as well as it can, but it is also highly customizable to match any requirements of an observatory. With ports for the XMPP server (and probably the file cache) open to the public, this enables a safe and easy remote access to the pyobs system.</p>
</sec>
</sec>
<sec id="s4">
<title>4 Affiliated Projects</title>
<p>There are a few projects with &#x201c;pyobs&#x201d; in their name that do not provide any new modules but some external services that are essential for operating a fully-autonomous telescope.</p>
<sec id="s4-1">
<title>4.1 Image Archive</title>
<p>In classic astronomy an observation consists of three steps:<list list-type="simple">
<list-item>
<p>1) Planning an observation, i.e., finding targets, defining filters and exposure times, evaluating best times for the observation, etc.</p>
</list-item>
<list-item>
<p>2) Actually performing the observation at the telescope.</p>
</list-item>
<list-item>
<p>3) Calibrating and analyzing the data.</p>
</list-item>
</list>
</p>
<p>Nowadays it is absolutely possible to automate all three and avoid human interaction at all. While this topic goes far beyond the scope of this paper, we want to mention that a full automation does not only work for large surveys, but also for small telescopes in the middle of a town like G&#xf6;ttingen (see Masur et al., in prep). However, for robotic observations at least the second step falls away from the observer&#x2019;s responsibility, but also parts of step one (defining observing times) and three (calibrate data). In that case, probably not knowing exactly when an observation was taken, an efficient way to find data becomes more important.</p>
<p>This is where an image archive comes into play. There is the LCO science archive<xref ref-type="fn" rid="fn13">
<sup>13</sup>
</xref>,<xref ref-type="fn" rid="fn14">
<sup>14</sup>
</xref> to use, but it stores the images in Amazon AWS S3, while we wanted to store data locally. So we developed our own backend, which also supports the LCO API, and took parts of the LCO web frontend with permission and adapted it to our needs. We also added a HTTP endpoint for uploading images. Within pyobs there are classes for both an easy upload using the VFS (via <monospace>ArchiveFile</monospace>), and a full wrapper for accessing the archive in <monospace>PyobsArchive</monospace>.</p>
<p>
<xref ref-type="fig" rid="F6">Figure 6</xref> shows a screenshot of the archive that we use for the two MONET telescopes and the IAG50&#xa0;cm telescope. On the left, there is a list of options to filter the data by. On the right is the list of images matching the selected criteria. More details&#x2013;including connected data (for calibrated images), a link to the FITS headers and a thumbnail preview&#x2013;can be accessed by clicking on the plus symbol. Single or multiple images can also easily be downloaded using this web frontend.</p>
<fig id="F6" position="float">
<label>FIGURE 6</label>
<caption>
<p>Screenshot of the pyobs-archive instance that we use for the MONET telescopes and the IAG50&#xa0;cm telescope.</p>
</caption>
<graphic xlink:href="fspas-09-891486-g006.tif"/>
</fig>
</sec>
<sec id="s4-2">
<title>4.2 Weather Aggregator</title>
<p>In <xref ref-type="sec" rid="s3-4-1">Section 3.4.1</xref> we already mentioned a project for aggregating data from different weather stations and evaluate the values in order to determine, whether the weather is good for observing. <xref ref-type="fig" rid="F7">Figure 7</xref> shows two screenshots from pyobs-weather as used by the IAG50&#xa0;cm telescope.</p>
<fig id="F7" position="float">
<label>FIGURE 7</label>
<caption>
<p>Two screenshots from the pyobs-weather web frontend for the IAG 50&#xa0;cm telescope. The main page is shown on the left and the Sensors page on the right.</p>
</caption>
<graphic xlink:href="fspas-09-891486-g007.tif"/>
</fig>
<p>On the left, the main page is shown, with average values from all sensor types as well as plots for the current night (top), weather status (green and red shaded areas in the plot below, indicating good or bad) and solar altitude (yellow line in same plot), and plots for all sensors, grouped by type (i.e., temperature, humidity, etc). On the right, the Sensors page is shown with current values for all sensors from all stations, times of last changes (for evaluated sensors, see below) and comments on the current status. There is a public API for the weather data, which can easily be accessed via the weather module (see <xref ref-type="sec" rid="s3-4-1">Section 3.4.1</xref>).</p>
<p>The system is fully customizable. The basic unit in pyobs-weather is a <italic>station</italic>, which usually defines a single physical weather station. There are some station classes already present, of which some are more generic (getting data from a MySQL or CSV table) and some are specific to the observatories where our telescopes are located (e.g., for the weather station on Mt. Locke at McDonald Observatory). Additional classes for more stations can easily be added. There are three special stations that do not represent an actual weather station: <monospace>Current</monospace> contains the current average values from all stations, while <monospace>Average</monospace> keeps a 5-min average. Finally, <monospace>Observer</monospace> calculates current conditions, which, at the moment, is only the solar altitude.</p>
<p>Every station then contains one or more <italic>sensors</italic>, which provide values for a sensor of a given type: temperature, relative humidity, pressure, wind speed and direction, particle count, rain, sky temperatures, or solar altitude. To each sensor, one or more <italic>evaluators</italic> can be attached, which take the current value and decide whether it allows for observations or not. Currently, pyobs-weather offers four different kinds of evaluators:<list list-type="simple">
<list-item>
<p>&#x2022; A <monospace>Boolean</monospace> is a simple logic evaluator, which is <monospace>True</monospace> if the sensor value is <monospace>True</monospace>, and vice versa&#x2013;or the opposite, if <monospace>invert</monospace> is set to <monospace>True</monospace>.</p>
</list-item>
<list-item>
<p>&#x2022; <monospace>Switch</monospace> is a simple switch, which is <monospace>True</monospace>, if the sensor value is above a given <monospace>threshold</monospace>, and vice versa. And, again, the other way around, if <monospace>invert</monospace> is set to <monospace>True</monospace>.</p>
</list-item>
<list-item>
<p>&#x2022; A <monospace>Schmitt</monospace> trigger is similar to a <monospace>Switch</monospace>, but it takes two values: for it to become <monospace>True</monospace>, the sensor value must be below a given <monospace>good</monospace> value, but to become <monospace>False</monospace> again, it must rise above a given <monospace>bad</monospace> value.</p>
</list-item>
<list-item>
<p>&#x2022; Sensor values have a valid flag, which is mostly used (and set to False), if the value is older than 5&#xa0;minutes. The Valid evaluator only evaluates to True, if the value is valid.</p>
</list-item>
</list>
</p>
<p>As an example, we assume getting relative humidities from two weather stations. For both we would typically set a <monospace>Schmitt</monospace> evaluator with values like <monospace>good</monospace> &#x3d; 80 and <monospace>bad</monospace> &#x3d; 85, which means that the weather is marked as bad, if the humidity rises above 85%, but is only marked good again, if the humidity falls below 80%. It would not be a good idea to attach a <monospace>Valid</monospace> evaluator to both, since weather stations can break. However, we still always want a valid reading for the humidity, so we assign it to the humidity sensor in the <monospace>Current</monospace> station. That way, if we get no valid value at all, the weather is marked as bad. Evaluators on the <monospace>Average</monospace> station are never evaluated, but they are used for color coding the plots, i.e., mark areas that would mean bad weather.</p>
<p>Every sensor can also have a delay before switching from good to bad or vice versa. This can be used so that, e.g., the rain sensor only reports good weather if the last rain was at least an hour ago. Or, the other way around, a sensor that tends to flapping, i.e. wrongly reports bad weather for a short time before going back to normal, could be set to switch to bad only if this condition lasts for a given time.</p>
</sec>
<sec id="s4-3">
<title>4.3 Astrometry</title>
<p>Getting astrometric solutions for images (i.e., &#x201c;plate-solving&#x201d; them) is a task required at multiple occasions (see, e.g., <xref ref-type="sec" rid="s3-3-3">Sections 2.5 and 3.3.3</xref>). For this we use a self-hosted version of <ext-link ext-link-type="uri" xlink:href="http://Astrometry.net">Astrometry.net</ext-link> (<xref ref-type="bibr" rid="B26">Lang et al., 2010</xref>), adding a HTTP interface for accessing its service. Similar to Astrometry. net&#x2019;s own web service, <xref ref-type="fn" rid="fn15">
<sup>15</sup>
</xref> it accepts a list of X/Y positions of stars on an image, but in addition some parameters for the fit can be provided, like a first guess for the coordinates and an estimate for the plate-scale. A successful call returns FITS header entries that can be added to an existing file in order to get a valid world coordinate system (WCS). The whole process usually takes well below one second. pyobs provides an image processor that uses this web service for easy use in a pipeline (see <xref ref-type="sec" rid="s2-5">Section 2.5</xref>).</p>
</sec>
</sec>
<sec id="s5">
<title>5 Full Robotic Mode</title>
<p>With everything described so far, we already have a working observatory. We can control all devices, automate some things, and remotely control the system with the GUI. All that is needed for a fully autonomous telescope is some piece of software that coordinates everything. These robotic systems come in all shapes and colors: from a rather simple survey mode, in which a pre-defined list of targets is executed from top to bottom, probably all to be done with the same settings, whenever the conditions are right, to a system with user-defined tasks, maybe multiple instruments, and a scheduler that tries to fit all together.</p>
<sec id="s5-1">
<title>5.1 Scheduling</title>
<p>The most simple robotic system imaginable is a simple list of targets that are to be observed one after the other, top to bottom. An algorithm for that might look like this:<list list-type="simple">
<list-item>
<p>1. Select a target from a list, probably the first one.</p>
</list-item>
<list-item>
<p>2. Move the telescope to the given coordinates.</p>
</list-item>
<list-item>
<p>3. Take an image and store it.</p>
</list-item>
<list-item>
<p>4. Repeat.</p>
</list-item>
</list>
</p>
<p>A system like this still has some other things to take care of, e.g., open up at dusk (for night observations) and close down at dawn&#x2013;or when the weather gets bad. Any interruption (like daylight or rain) would just delay the selection of the next target. While very simple, this kind of system is suitable for many types of observations. There is no module implementing a survey mode in pyobs, but it could easily be added with very few lines of new code, specialized on the use case at hand.</p>
<p>This &#x201c;survey mode&#x201d; is also easily extendable, e.g., add an exposure time and a filter to the table of targets and set them before starting the exposure. However, the targets would still be observed in the order that they appear in the table. Therefore, the next step might be to filter the table of targets by visibility and sort it by some kind of priority. If we do that every time the system is idle, we get some kind of &#x201c;just-in-time&#x201d; (JIT) scheduler, always picking the next target when needed, but never planning further ahead. Some control systems, like the one for STELLA on Tenerife (<xref ref-type="bibr" rid="B18">Granzer et al., 2012</xref>), developed this idea further and have been using it successfully for years. A JIT scheduler can be very powerful, because it can easily adapt to changing observing conditions like seeing or transparency and picks its next target accordingly.</p>
<p>There is, however, one major disadvantage for these kind of systems: selecting only the next targets means there is no full plan for the night (or day), so it may be difficult to impossible to predict, whether a specific target will be observed or not. It may even be difficult to decide, which parameters need to be changed in order to make sure the observation will take place. Furthermore, the selection of targets may never be &#x201c;optimal&#x201d;, i.e., it is challenging to fill the observing time with the best possible targets. For example, take an object <italic>A</italic> that can be observed at the beginning and at the end of the night (maybe a transit event). Another object <italic>B</italic> can only be observed at the beginning of the night. Even if <italic>A</italic> has a higher priority, it might be better to observe <italic>B</italic> first and then <italic>A</italic> at the end of the night.</p>
<p>An astronomer, going on an observing run, would probably plan the nights in advance and make a schedule, when to observe which target. This is an optimizing problem and so we can call these kinds of schedules &#x201c;optimal&#x201d;. This is a different approach to selecting targets and not as straightforward as the one for JIT schedulers described before. Luckily, there are free schedulers available for use, e.g., the adaptive scheduler developed by LCO<xref ref-type="fn" rid="fn16">
<sup>16</sup>
</xref> and the Astropy-affiliated project astroplan, just to name two. In principle, they all try to optimize the placement of observations for the whole night so that a given value is maximized, e.g., the total observing time or something like the time-integrated priorities of the tasks.</p>
<p>While the LCO scheduler can run fully independent from pyobs, there is a module based on astroplan: <monospace>Scheduler</monospace>. It takes schedulable tasks from a <monospace>TaskArchive</monospace> object, calculates a schedule, and writes it to a <monospace>TaskSchedule</monospace> object. A <monospace>TaskArchive</monospace> simply holds a list of tasks (in the form of <monospace>Task</monospace> objects) and returns them on request. The scheduler takes these tasks, converts them into astroplan&#x2019;s <monospace>ObservingBlocks</monospace>, applies given constraints, and starts the scheduler. The result is a time table, giving start and end times for all scheduled tasks, which is passed to the <monospace>TaskSchedule</monospace>, storing it to be accessed by the robotic telescope system.</p>
<p>All these three classes (<monospace>TaskArchive, TaskSchedule, and Task</monospace>) are abstract and need specific implementations for a method to store tasks and schedule. The implementation coming with pyobs is one tailored to be used with the LCO observing portal, but access to other task databases can easily be added.</p>
<p>Furthermore, this gives a simple framework for changing the used scheduler at a later time. The one implemented in astroplan is a &#x201c;greedy&#x201d; one, i.e., it schedules the task with the highest priority first, then the one with the next lower priority, and so on. While it ensures that the highest priority target is observed, this is not true for all other targets. Thus, the result of a &#x201c;greedy&#x201d; scheduler is still far away from an optimal one. However, changing the actual scheduler will not affect the rest of the robotic system at all.</p>
</sec>
<sec id="s5-2">
<title>5.2 LCO Observing Portal</title>
<p>The central part of the LCO observing portal is a database, mainly storing tasks, schedules, and observations, and a HTTP REST interface for accessing it&#x2013;see details about the API on LCO&#x2019;s developers page<xref ref-type="fn" rid="fn17">
<sup>17</sup>
</xref>.</p>
<p>When the portal is set up correctly and running, a new account must be created with &#x201c;Staff&#x201d; permissions to access all the necessary endpoints. The security token for this account must be provided in the configuration of <monospace>LcoTaskArchive</monospace> and <monospace>LcoTaskSchedule</monospace>, which are the LCO-specific implementations of the classes discussed above. They both make use of LcoTask that simply stores the JSON object returned from the portal. These classes are enough to run the scheduler in connection with an LCO portal.</p>
<p>
<xref ref-type="fig" rid="F8">Figure 8</xref> shows the structure of a task in the LCO portal:<list list-type="simple">
<list-item>
<p>&#x2022; The top-most element is a request group, which has a name and belongs to a proposal. It can contain one or more requests.</p>
</list-item>
<list-item>
<p>&#x2022; A request contains a location (i.e., the telescope to use), one or more observation windows and one or more configurations.</p>
</list-item>
<list-item>
<p>&#x2022; A configuration stores settings for acquisition and guiding, observing constraints (airmass, moon distance, etc.), the target information and one or more instrument configurations.</p>
</list-item>
<list-item>
<p>&#x2022; Finally, an instrument configuration holds information like exposure time and count, and filter to use, all depending on the selected instrument.</p>
</list-item>
</list>
</p>
<fig id="F8" position="float">
<label>FIGURE 8</label>
<caption>
<p>Structure of a task in the LCO portal. While the green fields can occur only once, there can be multiple entries for the yellow fields (Request, Configuration, Window, InstrumentConfig).</p>
</caption>
<graphic xlink:href="fspas-09-891486-g008.tif"/>
</fig>
<p>Each of these elements also contains an <monospace>extra_params</monospace> field, which can be used for any extra information that is not supported by the default parameters.</p>
<p>Configurations have a <monospace>type</monospace> parameter, which will be important for running the task. The default value for an imaging camera would usually be <monospace>EXPOSE</monospace>, which just exposes as many images as given in the instrument configuration. Another possibility is <monospace>REPEAT_EXPOSE</monospace>, which loops all instrument configurations, until a given <monospace>repeat_duration</monospace> is reached. There are also other, more specific types, like <monospace>AUTO_FOCUS</monospace> for performing an auto-focus series.</p>
</sec>
<sec id="s5-3">
<title>5.3 Running Tasks</title>
<p>With the schedule in place, we actually need to observe the tasks. In pyobs this is done by a <monospace>TaskRunner</monospace>, which only has two methods: <monospace>can_run()</monospace>checks, whether a given task can run right now, and <monospace>run_task()</monospace> actually executes it. For this, pyobs uses the concept of &#x201c;scripts&#x201d;, which can be fully customized in the <monospace>runner</monospace> section of a configuration for a task runner. While the following will concentrate on running tasks from an LCO portal, implementations for other task archives should be easily implemented.</p>
<p>In the case of the LCO portal, the script to use is defined by the configuration type. A possible configuration might look like this:</p>
<p>
<monospace>runner:</monospace>
</p>
<p>&#x2003;<monospace>class: pyobs.robotic.TaskRunner</monospace>
</p>
<p>&#x2003;<monospace>scripts:</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>BIAS:</monospace>
</p>
<p>&#x2003;&#x2003;&#x2003;<monospace>class: pyobs.robotic.lco.scripts.LcoDefaultScript</monospace>
</p>
<p>&#x2003;&#x2003;&#x2003;<monospace>camera: sbig6303e</monospace>
</p>
<p>&#x2003;&#x2003;<monospace>EXPOSE:</monospace>
</p>
<p>
<monospace>&#x2003;&#x2003;&#x2003;class: pyobs.robotic.lco.scripts.LcoDefaultScript</monospace>
</p>
<p>&#x2003;&#x2003;&#x2003;<monospace>telescope: telescope</monospace>
</p>
<p>&#x2003;&#x2003;&#x2003;<monospace>filters: sbig6303e</monospace>
</p>
<p>&#x2003;&#x2003;&#x2003;<monospace>camera: sbig6303e</monospace>
</p>
<p>&#x2003;&#x2003;&#x2003;<monospace>roof: dome</monospace>
</p>
<p>&#x2003;&#x2003;&#x2003;<monospace>acquisition: acquisition</monospace>
</p>
<p>&#x2003;&#x2003;&#x2003;<monospace>autoguider: autoguider</monospace>
</p>
<p>This uses the same script (<monospace>LcoDefaultScript</monospace> in <monospace>robotic.lco.scripts</monospace>) for two different configuration types. The script checks internally, what kind of observation to perform. As usual, all parameters in the configuration are forwarded to the constructor of the given class&#x2013;in this case all of them are names of modules handling specific tasks. Note that a request can contain multiple configurations, and each might use a different script and might be observable or not.</p>
<p>An <monospace>LcoTask</monospace> checks and runs all configurations in a request. The procedure that the <monospace>LcoDefaultScript</monospace> runs for a single configuration looks like this:<list list-type="simple">
<list-item>
<p>&#x2022; If the configuration type is <monospace>EXPOSE</monospace>, which is a science observation, move the telescope to the given coordinates.</p>
</list-item>
<list-item>
<p>&#x2022; If a fine acquisition is requested, do it.</p>
</list-item>
<list-item>
<p>&#x2022; If guiding is requested, start it.</p>
</list-item>
<list-item>
<p>&#x2022; Now loop all instrument configurations and for each set the filter and binning and take the given number of images.</p>
</list-item>
</list>
</p>
<p>If the configuration type is <monospace>BIAS</monospace> or <monospace>DARK</monospace>, this procedure would simplify to just taking images.</p>
<p>In the case of this <monospace>LcoDefaultScript</monospace>, the script needs to know about internals of the task that are not available via the public interface of <monospace>Task</monospace>, so an LCO specific script is required. The same is true for the auto-focus script <monospace>LcoAutoFocusScript</monospace>. On the other hand, there are some scripts that do not need any information from the task and can therefore be run in any system, e.g. the <monospace>SkyFlats</monospace> script. When using this in a LCO environment, the configuration type <monospace>SCRIPT</monospace> is required and a special script <monospace>LcoScript</monospace>, which evaluates a <monospace>script</monospace> parameter in the <monospace>extra_params</monospace> of the configuration to delegate execution to another script. It can be configured like this:</p>
<p>
<monospace>runner:</monospace>
</p>
<p>
<monospace>class: pyobs.robotic.TaskRunner</monospace>
</p>
<p>
<monospace>scripts:</monospace>
</p>
<p>
<monospace>SCRIPT:</monospace>
</p>
<p>&#x2003;<monospace>class: pyobs.robotic.lco.scripts.LcoScript</monospace>
</p>
<p>&#x2003;<monospace>scripts:</monospace>
</p>
<p>
<monospace>skyflats:</monospace>
</p>
<p>
<monospace>class: pyobs.robotic.scripts.SkyFlats</monospace>
</p>
<p>
<monospace>[...]</monospace>
</p>
<p>Now, whenever the configuration type is <monospace>SCRIPT</monospace> and the <monospace>script</monospace> is set to <monospace>skyflats</monospace>, the given script <monospace>SkyFlats</monospace> is executed. The whole script system is designed to be as flexible as possible and should allow for writing custom scripts for any requirement.</p>
<p>For our solar telescope we also use a (modified) LCO portal, but the robotic mode is a lot simpler: there is no scheduler, but the task archive just requests the schedulable blocks and returns the one with the highest priority. This is possible, because all positions on the solar disc are visible as soon as the Sun is up in the sky. There is also a different default script that just moves the telescope and triggers the spectrograph.</p>
</sec>
<sec id="s5-4">
<title>5.4 The Mastermind</title>
<p>Using the scripts system, building a central module that runs them becomes very simple&#x2013;we call this module the &#x201c;mastermind&#x201d;. It creates a <monospace>TaskSchedule</monospace> and a <monospace>TaskRunner</monospace> from its configuration and then continuously gets the tasks from the former and executes them via the latter. It also sends events when starting and finishing a task and writes information about the task into the FITS headers of the images.</p>
<p>The whole system is flexible enough that we run two 1.2&#xa0;m and one 0.5&#xa0;m night telescope with it, as well as a 0.5&#xa0;m solar telescope&#x2013;however, for the last one the default scripts are not used (they use, e.g., a different coordinate system) and even the LCO portal had to be adapted for this use case. Nevertheless, the changes were minimal and we can use the same code base for all telescopes.</p>
<p>The solar telescope is also a good example on how to customize the mastermind. Since all functionality is included by referencing <italic>Python</italic> classes in the configuration, the whole execution of a task can be changed, even when sticking with the LCO portal&#x2013;with other task backends one needs to write own code anyway. It is completely possible to change the code to operate multiple instruments or even telescopes. For instance, we are currently adapting the system to calibrate three instruments at two telescopes simultaneously for MONET/S (see next Section).</p>
</sec>
</sec>
<sec id="s6">
<title>6 Telescopes</title>
<p>The Institute for Astrophysics and Geophysics in G&#xf6;ttingen (IAG) operates four telescopes, of which two are located within the faculty building, one is in Texas, and the last one is in South Africa. In this section we will describe the hardware for each one and their level of automation with pyobs.</p>
<sec id="s6-1">
<title>6.1 IAG 50&#xa0;cm</title>
<p>The IAG 50&#xa0;cm is a Cassegrain telescope located on the roof of the institute with a main mirror with 0.5&#xa0;m diameter and a focus length of 5&#xa0;m (f/10), housed in a classical rotating dome. The telescope is mainly used for educational purposes and public outreach. With the use of pyobs we now also use the rare days of good weather in G&#xf6;ttingen for science observations, but it is mainly a testing platform for the two MONET telescopes (see below). The main instrument is a SBIG STL-6303E with a pixel scale of 0.55 <italic>&#x201d;</italic>/px (with a focal reducer). Attached to the telescope is a smaller 110&#xa0;mm f/7 refracting telescope with a ZWO ASI 071 MC camera.</p>
<p>Dome, telescope and focusing unit are running with ASCOM and are connected to pyobs via <monospace>pyobs-alpaca</monospace>. The two cameras use their respective modules (<monospace>pyobs-sbig</monospace> and <monospace>pyobs-asi</monospace>). The other modules that we use perform the following tasks (for all see <xref ref-type="sec" rid="s3">Section 3</xref>):<list list-type="simple">
<list-item>
<p>&#x2022; Fine acquisition with both of the cameras,</p>
</list-item>
<list-item>
<p>&#x2022; auto-focus for the main telescope and camera,</p>
</list-item>
<list-item>
<p>&#x2022; flat-fielding for both cameras,</p>
</list-item>
<list-item>
<p>&#x2022; file cache and image writer and watcher,</p>
</list-item>
<list-item>
<p>&#x2022; scheduler and mastermind for robotic mode,</p>
</list-item>
<list-item>
<p>&#x2022; telegram bot,</p>
</list-item>
<list-item>
<p>&#x2022; weather from a connected <monospace>pyobs-weather</monospace> (see <xref ref-type="sec" rid="s4-2">Section 4.2</xref>) page.</p>
</list-item>
</list>
</p>
<p>The main telescope runs fully robotically with a copy of the LCO portal (shared with the MONET telescopes), while the smaller telescope is not yet supported in this mode. We are currently working on guiding with the small telescope and on implementing the necessary pointing model in pyobs. We are also currently adding a fiber pick-up to transfer the light from the main telescope to a spectrograph in the optical lab (see next section). For this, the guiding uses a camera from The Imaging Source pointed at the fiber pin hole in a 45&#xb0; mirror, which has already been tested (see <xref ref-type="fig" rid="F9">Figure 9</xref>, right).</p>
<fig id="F9" position="float">
<label>FIGURE 9</label>
<caption>
<p>Left: Mid-resolution resolved Sun fiber setup for the VVT: The full image of the Sun is re-imaged onto the fiber pickup mirror that is hosting a 525&#xa0;<italic>&#x3bc;</italic>m fiber (corresponding to a 32 arcsec field of view). The fiber leads to a Fourier-Transform-spectrograph. Behind the pickup mirror the light is again re-imaged, this time onto the guiding camera which is used by pyobs for both pointing and guiding. Right: CAD-model of the fiber-guiding unit for the 50&#xa0;cm telescope. Starlight is re-imaged onto a fiber-pickup mirror and the remaining light is redirected into the guiding camera, allowing for nearby stars to act as guidestars.</p>
</caption>
<graphic xlink:href="fspas-09-891486-g009.tif"/>
</fig>
</sec>
<sec id="s6-2">
<title>6.2 IAG Vakuum-Vertikalteleskop</title>
<p>The Vakuum-Vertikalteleskop (VVT) consists of a siderostat on the top of the faculty building, redirecting the light two stories down into the building, where the 0.5&#xa0;m primary mirror is reflecting the light back up one story and into the optical lab. It provides both a f/11 primary focus and a Gregory f/50 secondary output.</p>
<p>There are a total of six observing modes for the telescope, with five of them using pyobs for pointing and guiding using a custom module via an interface to its control system. These modes include different spatial resolved observing modes (with field of views between about 4 and 100 arcsec) and Sun-as-a-star integrated light modes. As an example, <xref ref-type="fig" rid="F9">Figure 9</xref> (left) shows a Zemax raytracing of the mid-resolution resolved Sun fiber setup. The light from the primary mirror is collimated and re-imaged onto a fiber pickup mirror and re-imaged a second time onto the CCD guiding camera that is used for acquisition and guiding via detecting the solar disk.</p>
<p>The light entering the fiber is sent to our Fourier-Transform-spectrograph (FTS), a Bruker IFS 125HR with a maximum resolving power of <inline-formula id="inf1">
<mml:math id="m1">
<mml:mo>&#x3e;</mml:mo>
<mml:mn>700,000</mml:mn>
</mml:math>
</inline-formula> at 600&#xa0;nm. For the FTS, another custom module is used for HTTP communication with a LabView instance, which in turn is connected to the instrument software OPUS. For more details on the resolved Sun setup&#x2013;see <xref ref-type="bibr" rid="B37">Sch&#xe4;fer et al. (2020b)</xref>, and for more details on the coupling into the FTS see <xref ref-type="bibr" rid="B36">Sch&#xe4;fer et al. (2020a)</xref>. We are currently commissioning the fully robotic mode, based on a modified LCO portal, which now accepts coordinates in the Stonyhurst Heliographic system (HGS).</p>
</sec>
<sec id="s6-3">
<title>6.3 MONET/N</title>
<p>The two MONET Alt/Az telescopes (<xref ref-type="bibr" rid="B21">Hessman, 2001</xref>; <xref ref-type="bibr" rid="B10">Bischoff et al., 2006</xref>) have (almost) identical hardware with a 1.2&#xa0;m main mirror at f/7. They were optimized for fast operations with up to 10&#xb0;/s on both axes and therefore also have a clam-shell roof that opens completely. The northern telescope, MONET/N, is located at McDonald Observatory in Western Texas, United States the process of designing a fiber-fed high resolution spectrograph for high-precision radial velocity observations of G-type stars on the m/s-level.</p>
<p>The level of automation is about the same as for the IAG 50&#xa0;cm, with the exception of the piggyback telescope.</p>
</sec>
<sec id="s6-4">
<title>6.4 MONET/S</title>
<p>With MONET/S, located at the South African Astronomical Observatory (SAAO) near Sutherland, South Africa, having mostly the same hardware as MONET/N, there are still some differences. The science camera is currently a FLI PL230 and there is a 0.25&#xa0;m f/8 piggyback telescope mounted at the (unused) second Nasmyth port, with a SBIG STX-8300M camera attached to a Gemini derotator and focuser. Furthermore, outside the field of view of the main camera we installed a pickup for a fiber bundle leading to MORISOT, a low-budget, low-resolution spectrograph.</p>
<p>Again, the level of automation is similar to its twin in Texas and the IAG 50&#xa0;cm. <xref ref-type="fig" rid="F10">Figure 10</xref> shows an illustration of all pyobs module running at MONET/S, how they are distributed over several computers, and how they are connected to the actual hardware. As one can see, there is an additional module for the derotator of the piggyback telescope that we will publish as soon as it is fully tested. Acquisition (with an offset) and guiding of the spectrograph is supposed to be done with the science camera, and we hope to be able to do parallel photometry of the target with the piggyback. The custom module for the roof simply calls HTTP REST endpoints on our roof controller, and <monospace>BonnShutter</monospace> continuously checks the health of our Bonn shutter, and resets it, if any error occurs.</p>
<fig id="F10" position="float">
<label>FIGURE 10</label>
<caption>
<p>List of all pyobs modules that are currently running at MONET/S and how they are distributed to four different computers. Marked in green are those modules that are available in pyobs-core or one of the additional packages. The three other modules are custom implementations for the given hardware.</p>
</caption>
<graphic xlink:href="fspas-09-891486-g010.tif"/>
</fig>
<p>When everything is finished, there will probably be three modules for acquisition (one for each instrument) and three modules for guiding: science-frame auto-guiding on the main camera, guiding via piggyback, and guiding via science camera for the spectrograph. There will also be auto-focusing for both telescopes and flat-fielding for all three instruments. A challenge will be to calibrate all three instruments during twilight, but with the flexible scripts in the robotic part of pyobs, this should not be too much of a problem.</p>
</sec>
</sec>
<sec id="s7">
<title>7 Development</title>
<p>The development of pyobs is completely public, all the projects are hosted on GitHub and we use its &#x201c;Issues&#x201d; page as a bug tracker. At the moment, all the code comes from a single institution. We would, however, love to see contributions from other people, and would be glad to accept pull requests.</p>
<p>Development for pyobs has two different sides: creating new modules or change existing ones is quickly done and we can include as many into the main package as we went, as long as they provide some functionality that is otherwise missing and required by some observatories. Some possible extensions that come to mind are, e.g., support for dithering, focus offsets with respect to filters (which is already supported by the <monospace>FocusModel</monospace> module), guiding with PHD2, and so on.</p>
<p>Changing the core of pyobs (like, e.g., interfaces, error handling, communication, &#x2026; ) on the other hand would have to include some discussions in order to make sure that no existing code is broken and that it fits the general design philosophy of pyobs. Feel free to contact the author about any changes you would like to see.</p>
<p>As the leading zero in pyobs&#x2019; version number indicates, we do not assume pyobs to be in a &#x201c;stable&#x201d; condition, i.e. major (even breaking) changes can occur with every new version. However, the number of these changes has reduced significantly as of late. A potential user still needs to understand that things can and will change, which, of course, also gives the opportunity to actively shape those changes. As soon as we reach a stable version, we will fully implement semantic versioning and only apply breaking changes for new major versions.</p>
<p>For us, pyobs is mainly a tool for operating our four telescopes (see <xref ref-type="sec" rid="s6">Section 6</xref>). Therefore, keeping it in a state that is useful for us is our top priority. However, as we already showed with our solar telescope, we are willing to adapt existing code to work in new environments. That said, our time is limited, so we will not be able to give full-time support, but we continuously work on the documentation and try reply to emails and GitHub issues as quickly as possible. We would love to see a little community growing around pyobs that actively develops and supports it.</p>
<p>One group of observational astronomers that we have skipped over completely in this paper are the amateurs. Over the last decades they have built an amazing foundation of tools to build on, be it ASCOM, INDI, N.I.N.A.<xref ref-type="fn" rid="fn18">
<sup>18</sup>
</xref>, and so many others. Nowadays, amateur astronomers do some scientific work that many professional observatories cannot do anymore, e.g., long-time monitoring of variables. One great example as of late was the dimming of Betelgeuse, which is far too bright for larger telescopes.</p>
<p>However, at least in its current state, we do not believe that pyobs is a good tool for amateur astronomers. It was designed mainly for robotic observations at professional observatories. For instance, the GUI, which is an essential part for every remote setup, is mainly a maintenance tool for us and not used during regular observations. With pyobs being as open as it is, though, there is no reason, why it can not be developed into a direction that would make it more useful for amateurs as well. Therefore, any interested amateur astronomers are welcome to play around with pyobs, improve it, and contact us with any question or comment.</p>
</sec>
<sec id="s8">
<title>8 Summary and Outlook</title>
<p>In this paper we presented the observation control system pyobs. While pyobs itself is written in <italic>Python</italic> and highly depends on third-party packages, it can easily be extended by any programming language that supports the communication protocol XMPP. We showed that pyobs is highly customizable due to its configuration files, and provides a lot of functionality for robotic telescope operations out of the box: it has support for common tasks like flat-fielding and auto-focus series and connects to the open-source LCO observation portal for organizing tasks.</p>
<p>At the time of writing this paper, pyobs is available in version 0.17. As the leading zero suggests, we do not believe that it has reached a stable state, in which no major changes to any of its system will happen in the near future. However, at least the currently planned modifications are mostly minor, and we expect to publish a first release this year or soon thereafter&#x2013;so this should not keep anyone from using pyobs before that.</p>
<p>The list of planned improvements for the core of pyobs is long, but contains mostly minor items, which probably will not affect running systems. Some of the more major ones are:<list list-type="simple">
<list-item>
<p>&#x2022; The error handling (see <xref ref-type="sec" rid="s2-6">Section 2.6</xref>) is quite new and not used everywhere. It is missing, especially, in the robotic modules.</p>
</list-item>
<list-item>
<p>&#x2022; Some access control will be added, so that a module can allow some of its methods to be called only by authorized clients.</p>
</list-item>
<list-item>
<p>&#x2022; There already exist a few unit tests for the core package, but they are not covering everything, not even the most important parts.</p>
</list-item>
<list-item>
<p>&#x2022; New interfaces (see <xref ref-type="sec" rid="s2-2-2">Section 2.2.2</xref>) will be added&#x2013;e.g., for supporting to track non-sidereal targets &#x2013;, which might make it necessary to change existing ones.</p>
</list-item>
</list>
</p>
<p>While these items are for the core system, future development will mainly concentrate on additional modules. For instance, we would like to guide using a guiding telescope, which would require applying some pointing model to the offsets in order to compensate for different movements of the telescopes like bending and (thermal) stretching. We would also like to add a wrapper to the PHD2 guiding software, which would allow us to use this well-tested package in addition to our own guiding modules. Furthermore, as mentioned in <xref ref-type="sec" rid="s5-1">Section 5.1</xref>, a new (non-greedy) scheduler is high up on the wish list.</p>
<p>Using existing software was the goal for pyobs from the beginning. Instead of developing code from scratch it was built on top of widely used <italic>Python</italic> packages from the astronomical community and beyond. With pyobs-alpaca we already showed that we can bridge towards other protocols, and there probably will be a wrapper for INDI as well, which we can use to add devices for which an INDI driver already exists. There is also a plan to add wrappers for client software like Stellarium<xref ref-type="fn" rid="fn19">
<sup>19</sup>
</xref> or KStars, <xref ref-type="fn" rid="fn20">
<sup>20</sup>
</xref> that will make accessing a remote system easier, e.g. for students.</p>
<p>We will continue developing pyobs mainly for our own telescopes, but always trying to be as general as possible, so that it can be used by other observatories. The documentation is a good place to start playing around with pyobs and will be extended continuously. The author of this paper is looking forward to any contribution to pyobs, any comment and suggestion for improvement, and any question via email or GitHub issue tracker.</p>
</sec>
</body>
<back>
<sec id="s9" sec-type="data-availability">
<title>Data Availability Statement</title>
<p>The software presented in this article can be found here: <ext-link ext-link-type="uri" xlink:href="https://github.com/pyobs">https://github.com/pyobs</ext-link> <ext-link ext-link-type="uri" xlink:href="https://www.pyobs.org/">https://www.pyobs.org/</ext-link>.</p>
</sec>
<sec id="s10">
<title>Author Contributions</title>
<p>T-OH is the main developer of pyobs. FH, the former PI of MONET, developed a few of the device modules and gave helpful input for the big picture. KR and SM worked on adapting pyobs for the solar telescope. TM implemented the auto-guiding for single stars. SS is the PI of the FTS and responsible for a long list of feature requests and suggestions for improvements.</p>
</sec>
<sec sec-type="COI-statement" id="s11">
<title>Conflict of Interest</title>
<p>Author TM was employed by company TNG Technology Consulting GmbH.</p>
<p>The remaining authors declare that the research was conducted in the absence of any commercial or financial relationships that could be construed as a potential conflict of interest.</p>
</sec>
<sec sec-type="disclaimer" id="s12">
<title>Publisher&#x2019;s Note</title>
<p>All claims expressed in this article are solely those of the authors and do not necessarily represent those of their affiliated organizations, or those of the publisher, the editors and the reviewers. Any product that may be evaluated in this article, or claim that may be made by its manufacturer, is not guaranteed or endorsed by the publisher.</p>
</sec>
<ack>
<p>The development of pyobs and its modules was only possible by using several <italic>Python</italic> packages (in alphabetical order): Aiohttp, an asynchronous HTTP Client/Server for asyncio and <italic>Python</italic>.<xref ref-type="fn" rid="fn21">
<sup>21</sup>
</xref>. Astroplan, an open source <italic>Python</italic> package to help astronomers plan observations (<xref ref-type="bibr" rid="B30">Morris et al., 2018</xref>). Astropy,<xref ref-type="fn" rid="fn22">
<sup>22</sup>
</xref> a community-developed core <italic>Python</italic> package for Astronomy (<xref ref-type="bibr" rid="B2">Astropy Collaboration et al., 2013</xref>; <xref ref-type="bibr" rid="B1">Astropy Collaboration et al., 2018</xref>). Astroquery, a set of tools for querying astronomical web forms and databases (<xref ref-type="bibr" rid="B16">Ginsburg et al., 2019</xref>). asyncinotify, an async python inotify package.<xref ref-type="fn" rid="fn23">
<sup>23</sup>
</xref>. ccdproc, an Astropy package for image reduction (<xref ref-type="bibr" rid="B14">Craig et al., 2017</xref>). Cython, an optimising static compiler for the <italic>Python</italic> programming language.<xref ref-type="fn" rid="fn24">
<sup>24</sup>
</xref>. lmfit, Non-Linear Least-Squares Minimization and Curve-Fitting for <italic>Python</italic>.(<xref ref-type="bibr" rid="B31">Newville et al., 2021</xref>). Matplotlib, a comprehensive library for creating static, animated, and interactive visualizations in <italic>Python</italic>.<xref ref-type="fn" rid="fn25">
<sup>25</sup>
</xref>. Numpy, a fundamental package for scientific computing with <italic>Python</italic> (<xref ref-type="bibr" rid="B19">Harris et al., 2020</xref>). Pandas, an open source data analysis and manipulation tool (<xref ref-type="bibr" rid="B29">McKinney, 2010</xref>; <xref ref-type="bibr" rid="B32">Pandas Development Team, 2020</xref>). Paramiko, a pure-Python implementation of the SSHv2 protocol.<xref ref-type="fn" rid="fn26">
<sup>26</sup>
</xref>. Photutils, an Astropy package for detection and photometry of astronomical sources (<xref ref-type="bibr" rid="B11">Bradley et al., 2020</xref>). py-expression-eval, a <italic>Python</italic> mathematical expression evaluator.<xref ref-type="fn" rid="fn27">
<sup>27</sup>
</xref>. PyQt5, a set of <italic>Python</italic> bindings for Qt application framework.<xref ref-type="fn" rid="fn28">
<sup>28</sup>
</xref>. Python-aravis, a Pythonic interface to the auto-generated aravis bindings.<xref ref-type="fn" rid="fn29">
<sup>29</sup>
</xref>. Python-daemon, <italic>Python</italic> library to implement a well-behaved Unix daemon process.<xref ref-type="fn" rid="fn30">
<sup>30</sup>
</xref>. Python-telegram-bot, a <italic>Python</italic> wrapper for using Telegram.<xref ref-type="fn" rid="fn31">
<sup>31</sup>
</xref>. Python-zwoasi, a <italic>Python</italic> binding to the ZWO ASI version two library.<xref ref-type="fn" rid="fn32">
<sup>32</sup>
</xref>. PyYAML, a full-featured YAML framework for the <italic>Python</italic> programming language.<xref ref-type="fn" rid="fn33">
<sup>33</sup>
</xref>. Qasync, an implementation of the PEP 3156 event-loop to be used in PyQt applications.<xref ref-type="fn" rid="fn34">
<sup>34</sup>
</xref>. Scipy, a package for fundamental algorithms for scientific computing in <italic>Python</italic> (<xref ref-type="bibr" rid="B40">Virtanen et al., 2020</xref>). SEP, a <italic>Python</italic> and C library for Source Extraction and Photometry (<xref ref-type="bibr" rid="B7">Barbary, 2016</xref>; <xref ref-type="bibr" rid="B6">Barbary et al., 2017</xref>), based on Source Extractor (<xref ref-type="bibr" rid="B9">Bertin and Arnouts, 1996</xref>). Single-source, a single source of truth for version and name of a project.<xref ref-type="fn" rid="fn35">
<sup>35</sup>
</xref>. Slixmpp, an XMPP library for <italic>Python</italic> 3.7 &#x2b; .<xref ref-type="fn" rid="fn36">
<sup>36</sup>
</xref>. Some more packages are currently used by pyobs but not mentioned here, since they are going to be replaced soon. The GUI uses icons from the &#x201c;Crystal Clear&#x201d; set.<xref ref-type="fn" rid="fn37">
<sup>37</sup>
</xref>. Running our own instance of the LCO Observation Portal as well as connecting it to pyobs was made possible with the help of the great team at Las Cumbres Observatory (LCO). We also use parts of the frontend of their science archive. Both are parts of the LCO Observatory Control System (OCS).<xref ref-type="fn" rid="fn38">
<sup>38</sup>
</xref>
</p>
</ack>
<fn-group>
<fn id="fn1">
<label>1</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://ascom-standards.org">https://ascom-standards.org</ext-link>
</p>
</fn>
<fn id="fn2">
<label>2</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://indilib.org">https://indilib.org</ext-link>
</p>
</fn>
<fn id="fn3">
<label>3</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://github.com/pyobs">https://github.com/pyobs</ext-link>
</p>
</fn>
<fn id="fn4">
<label>4</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://www.pyobs.org">https://www.pyobs.org</ext-link>
</p>
</fn>
<fn id="fn5">
<label>5</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://www.ejabberd.im">https://www.ejabberd.im</ext-link>
</p>
</fn>
<fn id="fn6">
<label>6</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://www.igniterealtime.org/projects/openfire/">https://www.igniterealtime.org/projects/openfire/</ext-link>
</p>
</fn>
<fn id="fn7">
<label>7</label>
<p>see, e.g., <ext-link ext-link-type="uri" xlink:href="https://xmpp.org/software/libraries/">https://xmpp.org/software/libraries/</ext-link>
</p>
</fn>
<fn id="fn8">
<label>8</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://www.flicamera.com/downloads/FLI_SDK_Documentation.pdf">https://www.flicamera.com/downloads/FLI_SDK_Documentation.pdf</ext-link>
</p>
</fn>
<fn id="fn9">
<label>9</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://github.com/AravisProject/aravis">https://github.com/AravisProject/aravis</ext-link>
</p>
</fn>
<fn id="fn10">
<label>10</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://www.theimagingsource.de">https://www.theimagingsource.de</ext-link>
</p>
</fn>
<fn id="fn11">
<label>11</label>
<p>
<ext-link ext-link-type="uri" xlink:href="http://www.sonobs.de/company/company.html">http://www.sonobs.de/company/company.html</ext-link>
</p>
</fn>
<fn id="fn12">
<label>12</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://telegram.org">https://telegram.org</ext-link>
</p>
</fn>
<fn id="fn13">
<label>13</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://archive.lco.global/">https://archive.lco.global/</ext-link>
</p>
</fn>
<fn id="fn14">
<label>14</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://github.com/observatorycontrolsystem/science-archive">https://github.com/observatorycontrolsystem/science-archive</ext-link>
</p>
</fn>
<fn id="fn15">
<label>15</label>
<p>
<ext-link ext-link-type="uri" xlink:href="http://nova.astrometry.net">http://nova.astrometry.net</ext-link>
</p>
</fn>
<fn id="fn16">
<label>16</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://github.com/observatorycontrolsystem/adaptive_scheduler">https://github.com/observatorycontrolsystem/adaptive_scheduler</ext-link>
</p>
</fn>
<fn id="fn17">
<label>17</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://developers.lco.global">https://developers.lco.global</ext-link>
</p>
</fn>
<fn id="fn18">
<label>18</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://nighttime-imaging.eu/">https://nighttime-imaging.eu/</ext-link>
</p>
</fn>
<fn id="fn19">
<label>19</label>
<p>
<ext-link ext-link-type="uri" xlink:href="http://stellarium.org">http://stellarium.org</ext-link>
</p>
</fn>
<fn id="fn20">
<label>20</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://edu.kde.org/kstars/">https://edu.kde.org/kstars/</ext-link>
</p>
</fn>
<fn id="fn21">
<label>21</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://docs.aiohttp.org/">https://docs.aiohttp.org/</ext-link>
</p>
</fn>
<fn id="fn22">
<label>22</label>
<p>
<ext-link ext-link-type="uri" xlink:href="http://www.astropy.org">http://www.astropy.org</ext-link>
</p>
</fn>
<fn id="fn23">
<label>23</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://asyncinotify.readthedocs.io/">https://asyncinotify.readthedocs.io/</ext-link>
</p>
</fn>
<fn id="fn24">
<label>24</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://cython.org">https://cython.org</ext-link>
</p>
</fn>
<fn id="fn25">
<label>25</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://matplotlib.org">https://matplotlib.org</ext-link>
</p>
</fn>
<fn id="fn26">
<label>26</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://www.paramiko.org/">https://www.paramiko.org/</ext-link>
</p>
</fn>
<fn id="fn27">
<label>27</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://github.com/AxiaCore/py-expression-eval/">https://github.com/AxiaCore/py-expression-eval/</ext-link>
</p>
</fn>
<fn id="fn28">
<label>28</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://www.riverbankcomputing.com/software/pyqt/">https://www.riverbankcomputing.com/software/pyqt/</ext-link>
</p>
</fn>
<fn id="fn29">
<label>29</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://github.com/SintefManufacturing/python-aravis">https://github.com/SintefManufacturing/python-aravis</ext-link>
</p>
</fn>
<fn id="fn30">
<label>30</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://pagure.io/python-daemon/">https://pagure.io/python-daemon/</ext-link>
</p>
</fn>
<fn id="fn31">
<label>31</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://github.com/python-telegram-bot/python-telegram-bot">https://github.com/python-telegram-bot/python-telegram-bot</ext-link>
</p>
</fn>
<fn id="fn32">
<label>32</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://github.com/python-zwoasi/python-zwoasi">https://github.com/python-zwoasi/python-zwoasi</ext-link>
</p>
</fn>
<fn id="fn33">
<label>33</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://pyyaml.org">https://pyyaml.org</ext-link>
</p>
</fn>
<fn id="fn34">
<label>34</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://github.com/CabbageDevelopment/qasync">https://github.com/CabbageDevelopment/qasync</ext-link>
</p>
</fn>
<fn id="fn35">
<label>35</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://github.com/rabbit72/single-source">https://github.com/rabbit72/single-source</ext-link>
</p>
</fn>
<fn id="fn36">
<label>36</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://slixmpp.readthedocs.io/">https://slixmpp.readthedocs.io/</ext-link>
</p>
</fn>
<fn id="fn37">
<label>37</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://commons.wikimedia.org/wiki/Crystal_Clear">https://commons.wikimedia.org/wiki/Crystal_Clear</ext-link>
</p>
</fn>
<fn id="fn38">
<label>38</label>
<p>
<ext-link ext-link-type="uri" xlink:href="https://observatorycontrolsystem.github.io/">https://observatorycontrolsystem.github.io/</ext-link>
</p>
</fn>
</fn-group>
<ref-list>
<title>References</title>
<ref id="B1">
<citation citation-type="journal">
<collab>Astropy Collaboration</collab>
<person-group person-group-type="author">
<name>
<surname>Price-Whelan</surname>
<given-names>A. M.</given-names>
</name>
<name>
<surname>Price-Whelan</surname>
<given-names>A. M.</given-names>
</name>
<name>
<surname>Sip&#x151;cz</surname>
<given-names>B. M.</given-names>
</name>
<name>
<surname>G&#xfc;nther</surname>
<given-names>H. M.</given-names>
</name>
<name>
<surname>Lim</surname>
<given-names>P. L.</given-names>
</name>
<name>
<surname>Crawford</surname>
<given-names>S. M.</given-names>
</name>
<etal/>
</person-group> (<year>2018</year>). <article-title>The Astropy Project: Building an Open-Science Project and Status of the v2.0 Core Package</article-title>. <source>AJ</source> <volume>156</volume>, <fpage>123</fpage>. <pub-id pub-id-type="doi">10.3847/1538-3881/aabc4f</pub-id> </citation>
</ref>
<ref id="B2">
<citation citation-type="journal">
<collab>Astropy Collaboration</collab>
<person-group person-group-type="author">
<name>
<surname>Robitaille</surname>
<given-names>T. P.</given-names>
</name>
<name>
<surname>Robitaille</surname>
<given-names>T. P.</given-names>
</name>
<name>
<surname>Tollerud</surname>
<given-names>E. J.</given-names>
</name>
<name>
<surname>Greenfield</surname>
<given-names>P.</given-names>
</name>
<name>
<surname>Droettboom</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Bray</surname>
<given-names>E.</given-names>
</name>
<etal/>
</person-group> (<year>2013</year>). <article-title>Astropy: A Community Python Package for Astronomy</article-title>. <source>A&#x26;A</source> <volume>558</volume>, <fpage>A33</fpage>. <pub-id pub-id-type="doi">10.1051/0004-6361/201322068</pub-id> </citation>
</ref>
<ref id="B3">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Akerlof</surname>
<given-names>C. W.</given-names>
</name>
<name>
<surname>Kehoe</surname>
<given-names>R. L.</given-names>
</name>
<name>
<surname>McKay</surname>
<given-names>T. A.</given-names>
</name>
<name>
<surname>Rykoff</surname>
<given-names>E. S.</given-names>
</name>
<name>
<surname>Smith</surname>
<given-names>D. A.</given-names>
</name>
<name>
<surname>Casperson</surname>
<given-names>D. E.</given-names>
</name>
<etal/>
</person-group> (<year>2003</year>). <article-title>The ROTSE&#x2010;III Robotic Telescope System</article-title>. <source>Publ. Astron Soc. Pac</source> <volume>115</volume>, <fpage>132</fpage>&#x2013;<lpage>140</lpage>. <pub-id pub-id-type="doi">10.1086/345490</pub-id> </citation>
</ref>
<ref id="B4">
<citation citation-type="book">
<person-group person-group-type="author">
<name>
<surname>Alcock</surname>
<given-names>C.</given-names>
</name>
<name>
<surname>Axelrod</surname>
<given-names>T. S.</given-names>
</name>
<name>
<surname>Bennett</surname>
<given-names>D. P.</given-names>
</name>
<name>
<surname>Cook</surname>
<given-names>K. H.</given-names>
</name>
<name>
<surname>Park</surname>
<given-names>H. S.</given-names>
</name>
<name>
<surname>Griest</surname>
<given-names>K.</given-names>
</name>
<etal/>
</person-group> (<year>1992</year>). &#x201c;<article-title>The Search for Massive Compact Halo Objects with a (Semi) Robotic Telescope</article-title>,&#x201d; in <source>Robotic Telescopes in the 1990s</source>. <source>Vol. 103 of Astronomical Society of the Pacific Conference Series</source>. Editor <person-group person-group-type="editor">
<name>
<surname>Filippenko</surname>
<given-names>A. V.</given-names>
</name>
</person-group>, <fpage>193</fpage>&#x2013;<lpage>202</lpage>. </citation>
</ref>
<ref id="B5">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Antonelli</surname>
<given-names>L. A.</given-names>
</name>
<name>
<surname>Zerbi</surname>
<given-names>F. M.</given-names>
</name>
<name>
<surname>Chincarini</surname>
<given-names>G.</given-names>
</name>
<name>
<surname>Ghisellini</surname>
<given-names>G.</given-names>
</name>
<name>
<surname>Rodon&#xf2;</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Tosti</surname>
<given-names>G.</given-names>
</name>
<etal/>
</person-group> (<year>2003</year>). <article-title>The REM Telescope: a Robotic Facility to Monitor the Prompt Afterglow of Gamma Ray Bursts</article-title>. <source>Mem. Soc. Astron. Ital.</source> <volume>74</volume>, <fpage>304</fpage>. </citation>
</ref>
<ref id="B7">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Barbary</surname>
<given-names>K.</given-names>
</name>
</person-group> (<year>2016</year>). <article-title>Sep: Source Extractor as a Library</article-title>. <source>Joss</source> <volume>1</volume>, <fpage>58</fpage>. <pub-id pub-id-type="doi">10.21105/joss.00058</pub-id> </citation>
</ref>
<ref id="B8">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Bellm</surname>
<given-names>E. C.</given-names>
</name>
<name>
<surname>Kulkarni</surname>
<given-names>S. R.</given-names>
</name>
<name>
<surname>Barlow</surname>
<given-names>T.</given-names>
</name>
<name>
<surname>Feindt</surname>
<given-names>U.</given-names>
</name>
<name>
<surname>Graham</surname>
<given-names>M. J.</given-names>
</name>
<name>
<surname>Goobar</surname>
<given-names>A.</given-names>
</name>
<etal/>
</person-group> (<year>2019</year>). <article-title>The Zwicky Transient Facility: Surveys and Scheduler</article-title>. <source>PASP</source> <volume>131</volume>, <fpage>068003</fpage>. <pub-id pub-id-type="doi">10.1088/1538-3873/ab0c2a</pub-id> </citation>
</ref>
<ref id="B9">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Bertin</surname>
<given-names>E.</given-names>
</name>
<name>
<surname>Arnouts</surname>
<given-names>S.</given-names>
</name>
</person-group> (<year>1996</year>). <article-title>SExtractor: Software for Source Extraction</article-title>. <source>Astron. Astrophys. Suppl. Ser.</source> <volume>117</volume>, <fpage>393</fpage>&#x2013;<lpage>404</lpage>. <pub-id pub-id-type="doi">10.1051/aas:1996164</pub-id> </citation>
</ref>
<ref id="B10">
<citation citation-type="book">
<person-group person-group-type="author">
<name>
<surname>Bischoff</surname>
<given-names>K.</given-names>
</name>
<name>
<surname>Tuparev</surname>
<given-names>G.</given-names>
</name>
<name>
<surname>Hessman</surname>
<given-names>F. V.</given-names>
</name>
<name>
<surname>Nikolova</surname>
<given-names>I.</given-names>
</name>
</person-group> (<year>2006</year>). &#x201c;<article-title>MONET/North: a Very Fast 1.2m Robotic Telescope</article-title>,&#x201d; in <source>Society of Photo-Optical Instrumentation Engineers (SPIE) Conference Series</source>. <source>Vol. 6270 of Society of Photo-Optical Instrumentation Engineers (SPIE) Conference Series</source>. Editors <person-group person-group-type="editor">
<name>
<surname>Silva</surname>
<given-names>D. R.</given-names>
</name>
<name>
<surname>Doxsey</surname>
<given-names>R. E.</given-names>
</name>
</person-group>, <fpage>62701Q</fpage>. <pub-id pub-id-type="doi">10.1117/12.671433</pub-id> </citation>
</ref>
<ref id="B12">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Castro-Tirado</surname>
<given-names>A. J.</given-names>
</name>
<name>
<surname>Jel&#xed;nek</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Mateo Sanguino</surname>
<given-names>T. J.</given-names>
</name>
<name>
<surname>de Ugarte Postigo</surname>
<given-names>A.</given-names>
</name>
</person-group>
<collab>the BOOTES Team</collab> (<year>2004</year>). <article-title>BOOTES: A Stereoscopic Robotic Ground Support Facility</article-title>. <source>Astron. Nachr.</source> <volume>325</volume>, <fpage>679</fpage>. <pub-id pub-id-type="doi">10.1002/asna.200410333</pub-id> </citation>
</ref>
<ref id="B13">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Chromey</surname>
<given-names>F. R.</given-names>
</name>
<name>
<surname>Hasselbacher</surname>
<given-names>D. A.</given-names>
</name>
</person-group> (<year>1996</year>). <article-title>The Flat Sky: Calibration and Background Uniformity in Wide Field Astronomical Images</article-title>. <source>PASP</source> <volume>108</volume>, <fpage>944</fpage>. <pub-id pub-id-type="doi">10.1086/133817</pub-id> </citation>
</ref>
<ref id="B6">
<citation citation-type="book"> <person-group person-group-type="author">
<name>
<surname>Barbary</surname>
<given-names>K.</given-names>
</name>
<name>
<surname>Boone</surname>
<given-names>K.</given-names>
</name>
<name>
<surname>Craig</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Deil</surname>
<given-names>C.</given-names>
</name>
<name>
<surname>Rose</surname>
<given-names>B.</given-names>
</name>
</person-group> (<year>2017</year>). <source>Kbarbary/Sep: v1.0.2</source>. <pub-id pub-id-type="doi">10.5281/zenodo.896928</pub-id> </citation>
</ref>
<ref id="B11">
<citation citation-type="book"> <person-group person-group-type="author">
<name>
<surname>Bradley</surname>
<given-names>L.</given-names>
</name>
<name>
<surname>Sip&#x151;cz</surname>
<given-names>B.</given-names>
</name>
<name>
<surname>Robitaille</surname>
<given-names>T.</given-names>
</name>
<name>
<surname>Tollerud</surname>
<given-names>E.</given-names>
</name>
<name>
<surname>Vin&#xed;cius</surname>
<given-names>Z.</given-names>
</name>
<name>
<surname>Deil</surname>
<given-names>C.</given-names>
</name>
<etal/>
</person-group> (<year>2020</year>). <source>Astropy/Photutils: 1.0.0</source>. <pub-id pub-id-type="doi">10.5281/zenodo.4044744</pub-id> </citation>
</ref>
<ref id="B14">
<citation citation-type="book"> <person-group person-group-type="author">
<name>
<surname>Craig</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Crawford</surname>
<given-names>S.</given-names>
</name>
<name>
<surname>Seifert</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Robitaille</surname>
<given-names>T.</given-names>
</name>
<name>
<surname>Sip&#x151;cz</surname>
<given-names>B.</given-names>
</name>
<name>
<surname>Walawender</surname>
<given-names>J.</given-names>
</name>
<etal/>
</person-group> (<year>2017</year>). <source>Astropy/Ccdproc: v1.3.0.Post1</source>. <pub-id pub-id-type="doi">10.5281/zenodo.1069648</pub-id> </citation>
</ref>
<ref id="B31">
<citation citation-type="book"> <person-group person-group-type="author">
<name>
<surname>Newville</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Otten</surname>
<given-names>R.</given-names>
</name>
<name>
<surname>Nelson</surname>
<given-names>A.</given-names>
</name>
<name>
<surname>Ingargiola</surname>
<given-names>A.</given-names>
</name>
<name>
<surname>Stensitzki</surname>
<given-names>T.</given-names>
</name>
<name>
<surname>Allan</surname>
<given-names>D.</given-names>
</name>
<etal/>
</person-group> (<year>2021</year>). <source>Lmfit/Lmfit-Py: 1.0.3</source>. <pub-id pub-id-type="doi">10.5281/zenodo.5570790</pub-id> </citation>
</ref>
<ref id="B32">
<citation citation-type="book"> <person-group person-group-type="author">
<name>
<surname>Pandas Development Team</surname>
<given-names>T.</given-names>
</name>
</person-group> (<year>2020</year>). <source>Pandas-Dev/Pandas: Pandas</source>. <pub-id pub-id-type="doi">10.5281/zenodo.3509134</pub-id> </citation>
</ref>
<ref id="B15">
<citation citation-type="book">
<person-group person-group-type="author">
<name>
<surname>Filippenko</surname>
<given-names>A. V.</given-names>
</name>
<name>
<surname>Li</surname>
<given-names>W. D.</given-names>
</name>
<name>
<surname>Treffers</surname>
<given-names>R. R.</given-names>
</name>
<name>
<surname>Modjaz</surname>
<given-names>M.</given-names>
</name>
</person-group> (<year>2001</year>). &#x201c;<article-title>The Lick Observatory Supernova Search with the Katzman Automatic Imaging Telescope</article-title>,&#x201d; in <source>IAU Colloq. 183: Small Telescope Astronomy on Global Scales</source>. <source>Vol. 246 of Astronomical Society of the Pacific Conference Series</source>. Editors <person-group person-group-type="editor">
<name>
<surname>Paczynski</surname>
<given-names>B.</given-names>
</name>
<name>
<surname>Chen</surname>
<given-names>W. P.</given-names>
</name>
<name>
<surname>Lemme</surname>
<given-names>C.</given-names>
</name>
</person-group>, <volume>183</volume>, <fpage>121</fpage>&#x2013;<lpage>130</lpage>. <pub-id pub-id-type="doi">10.1017/s0252921100078738</pub-id> </citation>
</ref>
<ref id="B16">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Ginsburg</surname>
<given-names>A.</given-names>
</name>
<name>
<surname>Sip&#x151;cz</surname>
<given-names>B. M.</given-names>
</name>
<name>
<surname>Brasseur</surname>
<given-names>C. E.</given-names>
</name>
<name>
<surname>Cowperthwaite</surname>
<given-names>P. S.</given-names>
</name>
<name>
<surname>Craig</surname>
<given-names>M. W.</given-names>
</name>
<name>
<surname>Deil</surname>
<given-names>C.</given-names>
</name>
<etal/>
</person-group> (<year>2019</year>). <article-title>Astroquery: An Astronomical Web-Querying Package in Python</article-title>. <source>AJ</source> <volume>157</volume>, <fpage>98</fpage>. <pub-id pub-id-type="doi">10.3847/1538-3881/aafc33</pub-id> </citation>
</ref>
<ref id="B17">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Granzer</surname>
<given-names>T.</given-names>
</name>
</person-group> (<year>2006</year>). <article-title>STELLA and RoboTel - a Prototype for a Robotic Network?</article-title> <source>Astron. Nachr.</source> <volume>327</volume>, <fpage>792</fpage>&#x2013;<lpage>795</lpage>. <pub-id pub-id-type="doi">10.1002/asna.200610635</pub-id> </citation>
</ref>
<ref id="B18">
<citation citation-type="book">
<person-group person-group-type="author">
<name>
<surname>Granzer</surname>
<given-names>T.</given-names>
</name>
<name>
<surname>Weber</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Strassmeier</surname>
<given-names>K. G.</given-names>
</name>
</person-group> (<year>2012</year>). &#x201c;<article-title>The STELLA Control System</article-title>,&#x201d; in <source>Astronomical Society of India Conference Series</source>. <source>Vol. 7 of Astronomical Society of India Conference Series</source>, <fpage>247</fpage>. </citation>
</ref>
<ref id="B19">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Harris</surname>
<given-names>C. R.</given-names>
</name>
<name>
<surname>Millman</surname>
<given-names>K. J.</given-names>
</name>
<name>
<surname>van der Walt</surname>
<given-names>S. J.</given-names>
</name>
<name>
<surname>Gommers</surname>
<given-names>R.</given-names>
</name>
<name>
<surname>Virtanen</surname>
<given-names>P.</given-names>
</name>
<name>
<surname>Cournapeau</surname>
<given-names>D.</given-names>
</name>
<etal/>
</person-group> (<year>2020</year>). <article-title>Array Programming with NumPy</article-title>. <source>Nature</source> <volume>585</volume>, <fpage>357</fpage>&#x2013;<lpage>362</lpage>. <pub-id pub-id-type="doi">10.1038/s41586-020-2649-2</pub-id> </citation>
</ref>
<ref id="B20">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Henry</surname>
<given-names>G. W.</given-names>
</name>
<name>
<surname>Fekel</surname>
<given-names>F. C.</given-names>
</name>
<name>
<surname>Hall</surname>
<given-names>D. S.</given-names>
</name>
</person-group> (<year>1995</year>). <article-title>An Automated Search for Variability in Chromospherically Active Stars</article-title>. <source>AJ</source> <volume>110</volume>, <fpage>2926</fpage>. <pub-id pub-id-type="doi">10.1086/117740</pub-id> </citation>
</ref>
<ref id="B21">
<citation citation-type="book">
<person-group person-group-type="author">
<name>
<surname>Hessman</surname>
<given-names>F. V.</given-names>
</name>
</person-group> (<year>2001</year>). &#x201c;<article-title>MONET: a MOnitoring NEtwork of Telescopes</article-title>,&#x201d; in <source>IAU Colloq. 183: Small Telescope Astronomy on Global Scales</source>. <source>Vol. 246 of Astronomical Society of the Pacific Conference Series</source>. Editors <person-group person-group-type="editor">
<name>
<surname>Paczynski</surname>
<given-names>B.</given-names>
</name>
<name>
<surname>Chen</surname>
<given-names>W. P.</given-names>
</name>
<name>
<surname>Lemme</surname>
<given-names>C.</given-names>
</name>
</person-group>, <volume>183</volume>, <fpage>13</fpage>&#x2013;<lpage>21</lpage>. <pub-id pub-id-type="doi">10.1017/s0252921100078544</pub-id> </citation>
</ref>
<ref id="B22">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Hessman</surname>
<given-names>F. V.</given-names>
</name>
</person-group> (<year>2004</year>). <article-title>The MONET Project and beyond</article-title>. <source>Astron. Nachr.</source> <volume>325</volume>, <fpage>533</fpage>&#x2013;<lpage>536</lpage>. <pub-id pub-id-type="doi">10.1002/asna.200410274</pub-id> </citation>
</ref>
<ref id="B23">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Hidas</surname>
<given-names>M. G.</given-names>
</name>
<name>
<surname>Hawkins</surname>
<given-names>E.</given-names>
</name>
<name>
<surname>Walker</surname>
<given-names>Z.</given-names>
</name>
<name>
<surname>Brown</surname>
<given-names>T. M.</given-names>
</name>
<name>
<surname>Rosing</surname>
<given-names>W. E.</given-names>
</name>
</person-group> (<year>2008</year>). <article-title>Las Cumbres Observatory Global Telescope: A Homogeneous Telescope Network</article-title>. <source>Astron. Nachr.</source> <volume>329</volume>, <fpage>269</fpage>&#x2013;<lpage>270</lpage>. <pub-id pub-id-type="doi">10.1002/asna.200710950</pub-id> </citation>
</ref>
<ref id="B24">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Ivezi&#x107;</surname>
<given-names>&#x17d;.</given-names>
</name>
<name>
<surname>Kahn</surname>
<given-names>S. M.</given-names>
</name>
<name>
<surname>Tyson</surname>
<given-names>J. A.</given-names>
</name>
<name>
<surname>Abel</surname>
<given-names>B.</given-names>
</name>
<name>
<surname>Acosta</surname>
<given-names>E.</given-names>
</name>
<name>
<surname>Allsman</surname>
<given-names>R.</given-names>
</name>
<etal/>
</person-group> (<year>2019</year>). <article-title>LSST: From Science Drivers to Reference Design and Anticipated Data Products</article-title>. <source>ApJ</source> <volume>873</volume>, <fpage>111</fpage>. <pub-id pub-id-type="doi">10.3847/1538-4357/ab042c</pub-id> </citation>
</ref>
<ref id="B25">
<citation citation-type="book">
<person-group person-group-type="author">
<name>
<surname>Kuba&#x301;nek</surname>
<given-names>P.</given-names>
</name>
<name>
<surname>Jel&#xed;nek</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Nekola</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Topinka</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>&#x160;trobl</surname>
<given-names>J.</given-names>
</name>
<name>
<surname>Hudec</surname>
<given-names>R.</given-names>
</name>
<etal/>
</person-group> (<year>2004</year>). &#x201c;<article-title>RTS2 - Remote Telescope System, 2nd Version</article-title>,&#x201d; in <source>Gamma-Ray Bursts: 30 Years of Discovery</source>. <source>Vol. 727 of American Institute of Physics Conference Series</source>. Editors <person-group person-group-type="editor">
<name>
<surname>Fenimore</surname>
<given-names>E.</given-names>
</name>
<name>
<surname>Galassi</surname>
<given-names>M.</given-names>
</name>
</person-group>, <fpage>753</fpage>&#x2013;<lpage>756</lpage>. <pub-id pub-id-type="doi">10.1063/1.1810951</pub-id> </citation>
</ref>
<ref id="B26">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Lang</surname>
<given-names>D.</given-names>
</name>
<name>
<surname>Hogg</surname>
<given-names>D. W.</given-names>
</name>
<name>
<surname>Mierle</surname>
<given-names>K.</given-names>
</name>
<name>
<surname>Blanton</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Roweis</surname>
<given-names>S.</given-names>
</name>
</person-group> (<year>2010</year>). <article-title>Astrometry.net: Blind Astrometric Calibration of Arbitrary Astronomical Images</article-title>. <source>Astronomical J.</source> <volume>139</volume>, <fpage>1782</fpage>&#x2013;<lpage>1800</lpage>. <pub-id pub-id-type="doi">10.1088/0004-6256/139/5/1782</pub-id> </citation>
</ref>
<ref id="B27">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Law</surname>
<given-names>N. M.</given-names>
</name>
<name>
<surname>Kulkarni</surname>
<given-names>S. R.</given-names>
</name>
<name>
<surname>Dekany</surname>
<given-names>R. G.</given-names>
</name>
<name>
<surname>Ofek</surname>
<given-names>E. O.</given-names>
</name>
<name>
<surname>Quimby</surname>
<given-names>R. M.</given-names>
</name>
<name>
<surname>Nugent</surname>
<given-names>P. E.</given-names>
</name>
<etal/>
</person-group> (<year>2009</year>). <article-title>The Palomar Transient Factory: System Overview, Performance, and First Results</article-title>. <source>Publ. Astron Soc. Pac</source> <volume>121</volume>, <fpage>1395</fpage>&#x2013;<lpage>1408</lpage>. <pub-id pub-id-type="doi">10.1086/648598</pub-id> </citation>
</ref>
<ref id="B28">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Lipunov</surname>
<given-names>V. M.</given-names>
</name>
<name>
<surname>Kornilov</surname>
<given-names>V. G.</given-names>
</name>
<name>
<surname>Krylov</surname>
<given-names>A. V.</given-names>
</name>
<name>
<surname>Kuvshinov</surname>
<given-names>D. A.</given-names>
</name>
<name>
<surname>Gorbovskoy</surname>
<given-names>E. S.</given-names>
</name>
<name>
<surname>Tyurina</surname>
<given-names>N. V.</given-names>
</name>
<etal/>
</person-group> (<year>2007</year>). <article-title>Observations of Gamma-Ray Bursts and a Supernovae Search at the Robotic Telescope MASTER</article-title>. <source>Astronomical Astrophysical Trans.</source> <volume>26</volume>, <fpage>79</fpage>&#x2013;<lpage>86</lpage>. <pub-id pub-id-type="doi">10.1080/10556790701300462</pub-id> </citation>
</ref>
<ref id="B29">
<citation citation-type="confproc">
<person-group person-group-type="author">
<name>
<surname>McKinney</surname>
<given-names>W.</given-names>
</name>
</person-group> (<year>2010</year>). &#x201c;<article-title>Data Structures for Statistical Computing in Python</article-title>,&#x201d; in <conf-name>Proceedings of the 9th Python in Science Conference</conf-name>. Editors <person-group person-group-type="editor">
<name>
<surname>van der Walt</surname>
<given-names>S.</given-names>
</name>
<name>
<surname>Millman</surname>
<given-names>J.</given-names>
</name>
</person-group>, <fpage>56</fpage>&#x2013;<lpage>61</lpage>. <pub-id pub-id-type="doi">10.25080/Majora-92bf1922-00a</pub-id> </citation>
</ref>
<ref id="B30">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Morris</surname>
<given-names>B. M.</given-names>
</name>
<name>
<surname>Tollerud</surname>
<given-names>E.</given-names>
</name>
<name>
<surname>Sip&#x151;cz</surname>
<given-names>B.</given-names>
</name>
<name>
<surname>Deil</surname>
<given-names>C.</given-names>
</name>
<name>
<surname>Douglas</surname>
<given-names>S. T.</given-names>
</name>
<name>
<surname>Medina</surname>
<given-names>J. B.</given-names>
</name>
<etal/>
</person-group> (<year>2018</year>). <article-title>Astroplan: An Open Source Observation Planning Package in Python</article-title>. <source>AJ</source> <volume>155</volume>, <fpage>128</fpage>. <pub-id pub-id-type="doi">10.3847/1538-3881/aaa47e</pub-id> </citation>
</ref>
<ref id="B33">
<citation citation-type="book">
<person-group person-group-type="author">
<name>
<surname>Perlmutter</surname>
<given-names>S.</given-names>
</name>
<name>
<surname>Muller</surname>
<given-names>R. A.</given-names>
</name>
<name>
<surname>Newberg</surname>
<given-names>H. J. M.</given-names>
</name>
<name>
<surname>Pennypacker</surname>
<given-names>C. R.</given-names>
</name>
<name>
<surname>Sasseen</surname>
<given-names>T. P.</given-names>
</name>
<name>
<surname>Smith</surname>
<given-names>C. K.</given-names>
</name>
</person-group> (<year>1992</year>). &#x201c;<article-title>A Doubly Robotic Telescope: the Berkeley Automated Supernova Search</article-title>,&#x201d; in <source>Robotic Telescopes in the 1990s</source>. <source>Vol. 103 of Astronomical Society of the Pacific Conference Series</source>. Editor <person-group person-group-type="editor">
<name>
<surname>Filippenko</surname>
<given-names>A. V.</given-names>
</name>
</person-group>, <fpage>67</fpage>&#x2013;<lpage>71</lpage>. </citation>
</ref>
<ref id="B34">
<citation citation-type="book">
<person-group person-group-type="author">
<name>
<surname>Riddle</surname>
<given-names>R.</given-names>
</name>
<name>
<surname>Cromer</surname>
<given-names>J.</given-names>
</name>
<name>
<surname>Hale</surname>
<given-names>D.</given-names>
</name>
<name>
<surname>Henning</surname>
<given-names>J.</given-names>
</name>
<name>
<surname>Baker</surname>
<given-names>J.</given-names>
</name>
<name>
<surname>Milburn</surname>
<given-names>J.</given-names>
</name>
<etal/>
</person-group> (<year>2018</year>). &#x201c;<article-title>The Zwicky Transient Facility Robotic Observing System (Conference Presentation)</article-title>,&#x201d; in <source>Observatory Operations: Strategies, Processes, and Systems VII</source>. Editors <person-group person-group-type="editor">
<name>
<surname>Peck</surname>
<given-names>A. B.</given-names>
</name>
<name>
<surname>Benn</surname>
<given-names>C. R.</given-names>
</name>
<name>
<surname>Seaman</surname>
<given-names>R. L.</given-names>
</name>
</person-group> (<publisher-loc>Bellingham, WA, USA</publisher-loc>: <publisher-name>SPIE</publisher-name>). <pub-id pub-id-type="doi">10.1117/12.2312702</pub-id> </citation>
</ref>
<ref id="B35">
<citation citation-type="book">
<person-group person-group-type="editor">
<name>
<surname>Saint-Andre</surname>
<given-names>P.</given-names>
</name>
</person-group> (Editor) (<year>2004</year>). &#x201c;<article-title>Extensible Messaging and Presence Protocol (XMPP): Core</article-title>,&#x201d; <source>RFC 3920, RFC</source>. </citation>
</ref>
<ref id="B36">
<citation citation-type="book">
<person-group person-group-type="author">
<name>
<surname>Sch&#xe4;fer</surname>
<given-names>S.</given-names>
</name>
<name>
<surname>Huke</surname>
<given-names>P.</given-names>
</name>
<name>
<surname>Meyer</surname>
<given-names>D.</given-names>
</name>
<name>
<surname>Reiners</surname>
<given-names>A.</given-names>
</name>
</person-group> (<year>2020a</year>). &#x201c;<article-title>Fiber-coupling of Fourier Transform Spectrographs</article-title>,&#x201d; in <source>Ground-based and Airborne Instrumentation for Astronomy VIII</source>. Editors <person-group person-group-type="editor">
<name>
<surname>Evans</surname>
<given-names>C. J.</given-names>
</name>
<name>
<surname>Bryant</surname>
<given-names>J. J.</given-names>
</name>
<name>
<surname>Motohara</surname>
<given-names>K.</given-names>
</name>
</person-group> (<publisher-loc>Bellingham, WA, USA</publisher-loc>: <publisher-name>International Society for Optics and Photonics (SPIE)</publisher-name>), <volume>Vol. 11447</volume>, <fpage>784</fpage>&#x2013;<lpage>795</lpage>. <pub-id pub-id-type="doi">10.1117/12.2561599</pub-id> </citation>
</ref>
<ref id="B37">
<citation citation-type="book">
<person-group person-group-type="author">
<name>
<surname>Sch&#xe4;fer</surname>
<given-names>S.</given-names>
</name>
<name>
<surname>Royen</surname>
<given-names>K.</given-names>
</name>
<name>
<surname>Huster Zapke</surname>
<given-names>A.</given-names>
</name>
<name>
<surname>Ellwarth</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Reiners</surname>
<given-names>A.</given-names>
</name>
</person-group> (<year>2020b</year>). &#x201c;<article-title>Observing the Integrated and Spatially Resolved Sun with Ultra-high Spectral Resolution</article-title>,&#x201d; in <source>Ground-based and Airborne Instrumentation for Astronomy VIII</source>. Editors <person-group person-group-type="editor">
<name>
<surname>Evans</surname>
<given-names>C. J.</given-names>
</name>
<name>
<surname>Bryant</surname>
<given-names>J. J.</given-names>
</name>
<name>
<surname>Motohara</surname>
<given-names>K.</given-names>
</name>
</person-group> (<publisher-loc>Bellingham, WA, USA</publisher-loc>: <publisher-name>International Society for Optics and Photonics (SPIE)</publisher-name>), <volume>Vol. 11447</volume>, <fpage>2187</fpage>&#x2013;<lpage>2208</lpage>. <pub-id pub-id-type="doi">10.1117/12.2560156</pub-id> </citation>
</ref>
<ref id="B38">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Strassmeier</surname>
<given-names>K. G.</given-names>
</name>
<name>
<surname>Bartus</surname>
<given-names>J.</given-names>
</name>
<name>
<surname>Cutispoto</surname>
<given-names>G.</given-names>
</name>
<name>
<surname>Rodon&#xf3;</surname>
<given-names>M.</given-names>
</name>
</person-group> (<year>1997</year>). <article-title>Starspot Photometry with Robotic Telescopes</article-title>. <source>Astron. Astrophys. Suppl. Ser.</source> <volume>125</volume>, <fpage>11</fpage>&#x2013;<lpage>63</lpage>. <pub-id pub-id-type="doi">10.1051/aas:1997369</pub-id> </citation>
</ref>
<ref id="B39">
<citation citation-type="book">
<person-group person-group-type="author">
<name>
<surname>Street</surname>
<given-names>R. A.</given-names>
</name>
<name>
<surname>Pollaco</surname>
<given-names>D. L.</given-names>
</name>
<name>
<surname>Fitzsimmons</surname>
<given-names>A.</given-names>
</name>
<name>
<surname>Keenan</surname>
<given-names>F. P.</given-names>
</name>
<name>
<surname>Horne</surname>
<given-names>K.</given-names>
</name>
<name>
<surname>Kane</surname>
<given-names>S.</given-names>
</name>
<etal/>
</person-group> (<year>2003</year>). &#x201c;<article-title>SuperWASP: Wide Angle Search for Planets</article-title>,&#x201d; in <source>Scientific Frontiers in Research on Extrasolar Planets</source>. <source>Vol. 294 of Astronomical Society of the Pacific Conference Series</source>. Editors <person-group person-group-type="editor">
<name>
<surname>Deming</surname>
<given-names>D.</given-names>
</name>
<name>
<surname>Seager</surname>
<given-names>S.</given-names>
</name>
</person-group>, <fpage>405</fpage>&#x2013;<lpage>408</lpage>. </citation>
</ref>
<ref id="B40">
<citation citation-type="journal">
<person-group person-group-type="author">
<name>
<surname>Virtanen</surname>
<given-names>P.</given-names>
</name>
<name>
<surname>Gommers</surname>
<given-names>R.</given-names>
</name>
<name>
<surname>Oliphant</surname>
<given-names>T. E.</given-names>
</name>
<name>
<surname>Haberland</surname>
<given-names>M.</given-names>
</name>
<name>
<surname>Reddy</surname>
<given-names>T.</given-names>
</name>
<name>
<surname>Cournapeau</surname>
<given-names>D.</given-names>
</name>
<etal/>
</person-group> (<year>2020</year>). <article-title>SciPy 1.0: Fundamental Algorithms for Scientific Computing in Python</article-title>. <source>Nat. Methods</source> <volume>17</volume>, <fpage>261</fpage>&#x2013;<lpage>272</lpage>. <pub-id pub-id-type="doi">10.1038/s41592-019-0686-2</pub-id> </citation>
</ref>
</ref-list>
</back>
</article>