Skip to content

Satellite Scheduling Example

Download Notebook


Satellite Scheduling chooses an operational state for a satellite at each time step to maximise experiment (objective state) time subject to charge and data-storage constraints. It is critical for space mission planning.

import getpass
import os

from dotenv import load_dotenv
from luna_quantum.algorithms import SCIP

from luna_usecases.satellite_scheduling import (
    SatelliteSchedulingCollection,
    SatelliteSchedulingData,
    SatelliteSchedulingFormulation,
    SatelliteSchedulingInstance,
)

load_dotenv()
if "LUNA_API_KEY" not in os.environ:
    os.environ["LUNA_API_KEY"] = getpass.getpass("Enter your Luna API key: ")

Create Data

Define a satellite with 3 operational states over 5 time steps, with charge and data storage constraints.

data = SatelliteSchedulingData.from_values(
    n_time_steps=5,
    state_names=["Charging", "Experiment", "Downlink"],
    objective_state_name="Experiment",
    charge_rates=[1.0, -2.0, -1.0],
    data_rates=[0.0, 2.0, -2.0],
    charge_initial=6,
    charge_min=0,
    charge_max=6,
    data_initial=0,
    data_min=0,
    data_max=6,
    allowed_times=[[0, 1, 3, 4], [0, 1, 2, 3, 4], [1, 2, 3]],
)
print(data.to_string())
Satellite Scheduling Data:
  Time steps: 5
  States: ['Charging', 'Experiment', 'Downlink']
  Objective state: Experiment
  Charge: [0, 6], initial=6
  Data: [0, 6], initial=0
  Allowed times for 'Charging': [0, 1, 3, 4]
  Allowed times for 'Experiment': [0, 1, 2, 3, 4]
  Allowed times for 'Downlink': [1, 2, 3]

Plot Data

Visualize state constraints and resource limits over time.

data.plot()

<Axes: title={'center': 'Satellite Scheduling - Allowed States per Time Step'}, xlabel='Time step'>
png

Create Formulation

Schedule satellite states to maximize data collection while managing battery and storage.

formulation = SatelliteSchedulingFormulation()
print(formulation.to_string(data))
Satellite Scheduling Formulation:
  Time steps: 5
  States: 3
  Objective state: Experiment

Decision Variables:
  x[s,t] in {0,1} for s = 0, ..., 2, t = 0, ..., 4
  x[s,t] = 1 if state s is active at time t
  Total: 15 binary variables

Objective:
  maximize sum_t data_rates[Experiment] * x[objective_state, t]

Constraints:
  1. One state per time step (5 constraints):
     sum_s x[s,t] == 1  for all t = 0, ..., 4
     where s = 0, ..., 2 indexes ['Charging', 'Experiment', 'Downlink']
     and   t = 0, ..., 4 indexes the planning horizon
  2. Disallowed time/state combinations (3 constraints):
     x[s,t] == 0  for each (s, t) where t not in allowed_times[s]
  3. Cumulative charge within bounds (10 constraints):
     charge_min - charge_initial <= sum_{{t'=0}}^{{t}} sum_s charge_rates[s]*x[s,t'] <= charge_max - charge_initial  for all t
  4. Cumulative data within bounds (10 constraints):
     data_min - data_initial <= sum_{{t'=0}}^{{t}} sum_s data_rates[s]*x[s,t'] <= data_max - data_initial  for all t

Create Instance

Combine data and formulation into a solvable instance.

instance = SatelliteSchedulingInstance(data=data, formulation=formulation)
print(instance.to_string())
Data:Satellite Scheduling Data:
  Time steps: 5
  States: ['Charging', 'Experiment', 'Downlink']
  Objective state: Experiment
  Charge: [0, 6], initial=6
  Data: [0, 6], initial=0
  Allowed times for 'Charging': [0, 1, 3, 4]
  Allowed times for 'Experiment': [0, 1, 2, 3, 4]
  Allowed times for 'Downlink': [1, 2, 3]
Formulation:Satellite Scheduling Formulation:
  Time steps: 5
  States: 3
  Objective state: Experiment

Decision Variables:
  x[s,t] in {0,1} for s = 0, ..., 2, t = 0, ..., 4
  x[s,t] = 1 if state s is active at time t
  Total: 15 binary variables

Objective:
  maximize sum_t data_rates[Experiment] * x[objective_state, t]

Constraints:
  1. One state per time step (5 constraints):
     sum_s x[s,t] == 1  for all t = 0, ..., 4
     where s = 0, ..., 2 indexes ['Charging', 'Experiment', 'Downlink']
     and   t = 0, ..., 4 indexes the planning horizon
  2. Disallowed time/state combinations (3 constraints):
     x[s,t] == 0  for each (s, t) where t not in allowed_times[s]
  3. Cumulative charge within bounds (10 constraints):
     charge_min - charge_initial <= sum_{{t'=0}}^{{t}} sum_s charge_rates[s]*x[s,t'] <= charge_max - charge_initial  for all t
  4. Cumulative data within bounds (10 constraints):
     data_min - data_initial <= sum_{{t'=0}}^{{t}} sum_s data_rates[s]*x[s,t'] <= data_max - data_initial  for all t

Formulate Model

Translate the instance into a mathematical optimization model.

model = instance.formulate()

Solve and Interpret

Solve the model with SCIP and interpret the raw result into a use-case-specific solution.

scip = SCIP()
job = scip.run(model)
sol = job.result()
uc_solution = instance.interpret(sol)
print(uc_solution.to_string())
/Users/maximilianjanetschek/PycharmProjects/luna-usecases/.venv/lib/python3.13/site-packages/rich/live.py:260:
UserWarning: install "ipywidgets" for Jupyter support
  warnings.warn('install "ipywidgets" for Jupyter support')






2026-05-29 11:35:53 INFO     Sleeping for 5.0 seconds. Waiting and checking a function in a loop.




Satellite Scheduling Solution:
  Total data collected: 6.00
  Valid: True
  Schedule: [1 1 2 0 1]

Plot Solution

Visualize the optimal solution.

uc_solution.plot(data)

<Axes: title={'center': 'SAT Solution - data collected 6.0'}, xlabel='Time step'>
png

Collections

Generate a benchmark collection of random instances for batch processing.

collection = SatelliteSchedulingCollection.from_random(min_time_steps=4, max_time_steps=6, seed=42)
model = collection.instances[0].formulate()
print(model)
Model: satellite_scheduling<satellite_scheduling>
Maximize
  x_2_0 + x_2_1 + x_2_2 + x_2_3
Subject To
  one_state_t0: x_0_0 + x_1_0 + x_2_0 == 1
  one_state_t1: x_0_1 + x_1_1 + x_2_1 == 1
  one_state_t2: x_0_2 + x_1_2 + x_2_2 == 1
  one_state_t3: x_0_3 + x_1_3 + x_2_3 == 1
  disallowed_s0_t1: x_0_1 == 0
  charge_min_t0: 2 * x_0_0 - x_1_0 - x_2_0 >= -5
  charge_max_t0: 2 * x_0_0 - x_1_0 - x_2_0 <= 8
  data_min_t0: -2 * x_1_0 + x_2_0 >= -1
  data_max_t0: -2 * x_1_0 + x_2_0 <= 8
  charge_min_t1: 2 * x_0_0 + 2 * x_0_1 - x_1_0 - x_1_1 - x_2_0 - x_2_1 >= -5
  charge_max_t1: 2 * x_0_0 + 2 * x_0_1 - x_1_0 - x_1_1 - x_2_0 - x_2_1 <= 8
  data_min_t1: -2 * x_1_0 - 2 * x_1_1 + x_2_0 + x_2_1 >= -1
  data_max_t1: -2 * x_1_0 - 2 * x_1_1 + x_2_0 + x_2_1 <= 8
  charge_min_t2: 2 * x_0_0 + 2 * x_0_1 + 2 * x_0_2 - x_1_0 - x_1_1 - x_1_2
    - x_2_0 - x_2_1 - x_2_2 >= -5
  charge_max_t2: 2 * x_0_0 + 2 * x_0_1 + 2 * x_0_2 - x_1_0 - x_1_1 - x_1_2
    - x_2_0 - x_2_1 - x_2_2 <= 8
  data_min_t2: -2 * x_1_0 - 2 * x_1_1 - 2 * x_1_2 + x_2_0 + x_2_1 + x_2_2 >= -1
  data_max_t2: -2 * x_1_0 - 2 * x_1_1 - 2 * x_1_2 + x_2_0 + x_2_1 + x_2_2 <= 8
  charge_min_t3: 2 * x_0_0 + 2 * x_0_1 + 2 * x_0_2 + 2 * x_0_3 - x_1_0 - x_1_1
    - x_1_2 - x_1_3 - x_2_0 - x_2_1 - x_2_2 - x_2_3 >= -5
  charge_max_t3: 2 * x_0_0 + 2 * x_0_1 + 2 * x_0_2 + 2 * x_0_3 - x_1_0 - x_1_1
    - x_1_2 - x_1_3 - x_2_0 - x_2_1 - x_2_2 - x_2_3 <= 8
  data_min_t3: -2 * x_1_0 - 2 * x_1_1 - 2 * x_1_2 - 2 * x_1_3 + x_2_0 + x_2_1
    + x_2_2 + x_2_3 >= -1
  data_max_t3: -2 * x_1_0 - 2 * x_1_1 - 2 * x_1_2 - 2 * x_1_3 + x_2_0 + x_2_1
    + x_2_2 + x_2_3 <= 8
Binary
  x_0_0 x_0_1 x_0_2 x_0_3 x_1_0 x_1_1 x_1_2 x_1_3 x_2_0 x_2_1 x_2_2 x_2_3