Satellite Scheduling Example
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.
Create Formulation
Schedule satellite states to maximize data collection while managing battery and storage.
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.
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.
Plot Solution
Visualize the optimal solution.
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