Robotic mode (pyobs.robotic)

pyobs can operate a telescope fully autonomously. In robotic mode, a pool of pending observation tasks is continuously scheduled and executed without human intervention. This section describes the architecture of the robotic system and how its components fit together.

Architecture

The robotic system is built around five concepts:

Concept

Role

Task

A unit of work: what to observe, how long it takes, when it may run, and what script to execute.

Observation

A scheduled instance of a task: a task assigned a concrete start and end time.

Scheduler

Evaluates the pool of tasks and calculates which should run next, producing a sequence of Observation objects.

Mastermind

Watches the schedule and, at the right moment, hands each Observation to the TaskRunner for execution.

Script

The observing logic itself: move the telescope, take exposures, save images.

The flow through the system looks like this:

                   ┌─────────────┐
                   │ TaskArchive │  ← task pool (files, backend, LCO portal)
                   └──────┬──────┘
                          │ schedulable tasks
                          ▼
┌──────────────────────────────────────────────┐
│  Scheduler module                            │
│                                              │
│   for each time slot:                        │
│     evaluate Constraints  → gate (yes/no)    │
│     evaluate Merits       → rank (0..N)      │
│     pick highest-ranked eligible task        │
└──────────────────┬───────────────────────────┘
                   │ Observation (task + start + end)
                   ▼
          ┌─────────────────────┐
          │ ObservationArchive  │  ← persists the schedule
          └──────────┬──────────┘
                     │ next pending Observation
                     ▼
┌────────────────────────────────────┐
│  Mastermind module                 │
│                                    │
│   at scheduled time:               │
│     TaskRunner.run_task(task)      │
└────────────────┬───────────────────┘
                 │
                 ▼
          ┌─────────────┐
          │   Script    │  ← the actual observing logic
          └─────────────┘

Module layer vs. robotic layer

It helps to distinguish two layers:

The module layer (pyobs.modules.robotic) contains the long-running pyobs processes you start from YAML config files — Scheduler and Mastermind. These are full Module subclasses with comm, background tasks, and event subscriptions.

The robotic layer (pyobs.robotic) contains the data and logic objects that the modules orchestrate — Task, Script, Constraint, Merit, OnDemandScheduler, and the archive classes. These are either Object subclasses or pydantic models, not modules, and are configured as nested objects inside the module YAML.

This means a typical deployment YAML for the scheduler module looks like:

class: pyobs.modules.robotic.Scheduler    # ← module layer

scheduler:
  class: pyobs.robotic.scheduler.OnDemandScheduler  # ← robotic layer
  twilight: astronomical

tasks:
  class: pyobs.robotic.filesystem.YamlTaskArchive   # ← robotic layer
  path: /robotic/tasks/

schedule:
  class: pyobs.robotic.filesystem.YamlObservationArchive  # ← robotic layer
  path: /opt/pyobs/robotic/observations/

Tasks and scheduling

A Task carries four kinds of information:

  • Identityid, name, project, priority

  • Scheduling metadataduration, constraints, merits, target

  • Script config — the script block that defines what to do when the task runs

  • State — tracked indirectly via Observation objects in the ObservationArchive

Constraints answer a binary question: may this task run right now? If any constraint returns False, the task is excluded from consideration for that time slot entirely. Examples: airmass too high, sun still up, outside the allowed time window.

Merits answer a continuous question: how desirable is it to run this task right now? All merit values for a task are multiplied together (along with priority and project priority) to produce a single score. The task with the highest score in each time slot wins. Examples: transit timing, time since last observation, distance from a preferred window.

The clean separation between constraints (hard gates) and merits (soft ranking) means you can express complex scheduling policies entirely in YAML without writing any code. See Scheduling (pyobs.robotic.scheduler) for the full list of built-in constraints and merits.

Scheduler re-triggering

The Scheduler module recalculates the schedule whenever _need_update is set. This happens automatically in several situations:

  • The task pool changes (a task is added, removed, or modified in the TaskArchive)

  • A GoodWeatherEvent arrives, carrying an ETA for when observing can resume

  • A TaskStartedEvent arrives (if trigger_on_task_started: true)

  • A TaskFinishedEvent arrives (if trigger_on_task_finished: true)

  • The run() method is called manually (e.g. from the GUI)

To avoid submitting a stale schedule while a new one is being calculated, the scheduler submits the first task immediately as soon as it is found, then submits the rest once the full run completes. If a new update is requested mid-run, the partial results are discarded.

Scripts

A Script is the leaf of the system — it does the actual work. Scripts are pydantic models (not modules), so they are fully described by their YAML config and have access to comm, vfs, and observer injected at runtime.

A Script has two methods:

  • can_run(data) — called by the scheduler to check whether the script’s hardware is currently available. Return False if a required module is offline.

  • run(data) — called by the mastermind to execute the script. Receives a TaskData object giving access to the current task, the ObservationArchive, and the TaskArchive.

Built-in scripts cover common observing tasks (autofocus, sky flats, dark/bias frames) and control flow (sequential, parallel, conditional). See Scripts (pyobs.robotic.scripts) for details.

Archive implementations

Both TaskArchive and ObservationArchive are abstract base classes. pyobs-core ships three concrete implementations:

Implementation

Use case

pyobs.robotic.filesystem

Tasks and observations stored as YAML files on disk. The simplest setup — no external services required. Good for single-telescope systems.

pyobs.robotic.backend

Tasks and observations managed by the pyobs-robotic-backend HTTP service. Enables multi-telescope coordination and a web UI for queue management.

pyobs.robotic.lco

Integration with the Las Cumbres Observatory observation portal. Used for LCO-connected telescopes.

Further reading