Observation Scheduling
Efficient observation scheduling for any telescope is a challenge. This is an assignment problem for which a series of tasks, each with its own constraints, needs to be planned.
Computationnaly, this is a NP-hard problem: a solution is easy to verify (i.e., it can be done in a polynomial number of steps) but it is difficult to solve/find the optimal solution (i.e., possibly involving an exponential number of steps). Let’s assume that a planning consists of \(n_{\rm slots}\) time slots. Without any prior, the number of combinations \(N_c\) to fit \(k_{\rm obs}\) observations in this planning would be:
\(N_c\) becomes gigantic quickly. As an example, let’s consider \(k_{\rm obs} = 20\) observations to schedule over 30 days. Each day is divided with a 1-hour granularity, implying \(n_{\rm slots} = 30 \times 24 = 720\) time slots. These are low numbers compared to reality, and nonetheless amount to \(N_c = 10^{57}\) observation combinations to check for.
To solve this problem, nenupy
offers the schedule
module, which provides heuristic algorithms to reduce the number of checks to perform.
In addition, a dedicated genetic algorithm has been developped in order to further optimize the observation scheduling for NenuFAR.
The schedule
module consists of many different objects definitions that are needed to be loaded:
>>> from nenupy.schedule import (
>>> Schedule,
>>> ObsBlock,
>>> ReservedBlock,
>>> ESTarget,
>>> SSTarget,
>>> Constraints,
>>> ElevationCnst,
>>> AzimuthCnst,
>>> MeridianTransitCnst,
>>> LocalTimeCnst,
>>> TimeRangeCnst
>>> )
Other useful packages are loaded as well:
>>> from astropy.time import Time, TimeDelta
>>> import astropy.units as u
Schedule object
The framework object governing observation scheduling is Schedule
.
The arguments time_min
and time_max
define the planning time range edges.
dt
is the time granularity, or the time slot duration, at which the observations will be scheduled.
>>> schedule = Schedule(
>>> time_min=Time('2021-01-11 00:00:00'),
>>> time_max=Time('2021-01-15 00:00:00'),
>>> dt=TimeDelta(3600, format='sec')
>>> )
>>> schedule.plot(days_per_line=2)
A Schedule
can be displayed with the plot()
, for quick visualization of its status.
The time slots are separated by vertical lines.
Warning
The number of time slots, resulting from time_min
, time_max
and dt
values, highly impacts the computing time.
Reserved time slots
For some reason, some time slots within the Schedule
may be unavailable for observation programming (because of telescope maintenance, other observations already booked, …).
To represent this, a ReservedBlock
instance can be created and inserted in the schedule using the insert
method:
>>> schedule = Schedule(
>>> time_min=Time('2021-01-11 00:00:00'),
>>> time_max=Time('2021-01-15 00:00:00'),
>>> dt=TimeDelta(3600, format='sec')
>>> )
>>> maintenance = ReservedBlock(
>>> time_min=Time('2021-01-12 05:00:00'),
>>> time_max=Time('2021-01-13 16:00:00')
>>> )
>>> schedule.insert(maintenance)
>>> schedule.plot(days_per_line=2)
The schedule visualization displays such reserved blocks as hatched grey time windows.
The reverse operation can also be perfomed thanks to the set_free_slots()
method which sets the whole schedule as unavailable except for the time intervals given as inputs:
>>> schedule = Schedule(
>>> time_min=Time('2021-01-11 00:00:00'),
>>> time_max=Time('2021-01-15 00:00:00'),
>>> dt=TimeDelta(3600, format='sec')
>>> )
>>> schedule.set_free_slots(
>>> start_times=Time(['2021-01-12 10:00:00', '2021-01-14 08:32:00']),
>>> stop_times=Time(['2021-01-13 03:30:00', '2021-01-15 00:00:00'])
>>> )
Virtual Control Room bookings
NenuFAR time allocation is made thanks to time windows booking attribution to a given scientific project.
If one wants to schedule observations outside of these booking periods, the current booking can be dwonloaded through the VCR planning as a CSV file.
This file can then be used to instantiates a ReservedBlock
object via the from_VCR
method.
>>> schedule = Schedule(
>>> time_min=Time('2021-11-15 00:00:00'),
>>> time_max=Time('2021-11-20 00:00:00'),
>>> dt=TimeDelta(3600, format='sec')
>>> )
>>> reserved = ReservedBlock.from_VCR(file_name=".../2021-11-12_booking.csv")
>>> schedule.insert(reserved)
>>> schedule.plot(days_per_line=1)
The same file can also be used to globally set the schedule slots to unavailable except those belonging to a given Science Key Project.
The method match_booking()
allows one to define a schedule that matches the time slots allocated to a NenuFAR KP.
Observation request
Once the global planning configuration has been set, the user may define the observations to schedule.
Observation block
Observation requests are defined as ObsBlock
instances.
Later, they will be distributed efficiently in the schedule while taking into account observing constraints.
A minimal set of parameters are required during an ObsBlock
instantiation:
the
name
of the observation, for further reference,the NenuFAR scientific
program
,a celestial target (either fixed
ESTarget
in the equatorial grid or from the Solar SystemSSTarget
),the requested
duration
of the observation.
>>> vira = ObsBlock(
>>> name='Virgo A',
>>> program='es00',
>>> target=ESTarget.fromName('Vir A'),
>>> duration=TimeDelta(3600, format='sec')
>>> )
Note
Observation blocks will be booked in the schedule according to their duration
on an integer number of time slots defined by dt
.
Combining observation blocks
ObsBlock
objects accept +
and *
operations.
This enable observation block requests to be appended and duplicated, respectively.
Note, that it is computationnaly more efficient to use the duplicate block option (*
) instead of creating an other block with the same target and constraints.
>>> vira = ObsBlock(
>>> name='Virgo A',
>>> program='es00',
>>> target=ESTarget.fromName('Vir A'),
>>> duration=TimeDelta(3600, format='sec'),
>>> )
>>> cyga = ObsBlock(
>>> name='Cyg A',
>>> program='es00',
>>> target=ESTarget.fromName('Cyg A'),
>>> duration=TimeDelta(3600, format='sec'),
>>> )
>>> my_observations = vira*2 + cyga*3
Observation constraints
Automatic scheduling of astronomical observations relies on the definition of constraints that govern identification of optimal time periods to observe a given celestial target.
The different classes implemented in the constraints
module allow for an easy way of specifying these constraints.
Available constraints
Each Constraint
object is callable
and requires an argument of type ESTarget
or SSTarget
in order to evaluate the constraint’s ‘score’ over a given time range.
Available constraints are:
|
Elevation constraint |
|
Meridian Transit constraint |
|
Added in version 1.2.0. |
|
|
|
Added in version 1.2.0. |
|
Added in version 1.2.0. |
|
Adding constraints
>>> vira = ObsBlock(
>>> name='Virgo A',
>>> program='es00',
>>> target=ESTarget.fromName('Vir A'),
>>> duration=TimeDelta(3600, format='sec'),
>>> constraints=Constraints(ElevationCnst(elevationMin=10))
>>> )
>>> constraints = Constraints(ElevationCnst(10), MeridianTransitCnst())
>>> vira = ObsBlock(
>>> name='Virgo A',
>>> program='es00',
>>> target=ESTarget.fromName('Vir A'),
>>> duration=TimeDelta(3600, format='sec'),
>>> constraints=constraints
>>> )
Evaluating contraints on a schedule
>>> schedule = Schedule(
>>> time_min=Time('2021-11-15 00:00:00'),
>>> time_max=Time('2021-11-19 00:00:00'),
>>> )
>>> vira = ObsBlock(
>>> name='Virgo A',
>>> program='es00',
>>> target=ESTarget.fromName('Vir A'),
>>> duration=TimeDelta(3600*4, format='sec'),
>>> )
>>> schedule.insert(vira)
>>> schedule.book( optimize=False )
>>> schedule.observation_blocks[0].constraints.plot()
>>> schedule = Schedule(
>>> time_min=Time('2021-11-15 00:00:00'),
>>> time_max=Time('2021-11-19 00:00:00'),
>>> )
>>> vira = ObsBlock(
>>> name='Virgo A',
>>> program='es00',
>>> target=ESTarget.fromName('Vir A'),
>>> duration=TimeDelta(3600*4, format='sec'),
>>> constraints=Constraints(ElevationCnst(20), AzimuthCnst(100))
>>> )
>>> schedule.insert(vira)
>>> schedule.book( optimize=False )
>>> schedule.plot(days_per_line=2)
>>> schedule.observation_blocks[0].plot()
Scheduling in practice
>>> schedule = Schedule(
>>> time_min=Time('2021-11-17 00:00:00'),
>>> time_max=Time('2021-11-18 00:00:00'),
>>> dt=TimeDelta(60*30, format='sec')
>>> )
>>> sun = ObsBlock(
>>> name='Sun observation',
>>> program='es11',
>>> target=SSTarget.fromName('Sun'),
>>> duration=TimeDelta(2*3600, format='sec'),
>>> constraints=Constraints( ElevationCnst(elevationMin=12) )
>>> )
>>> schedule.insert(
>>> ReservedBlock(
>>> time_min=Time('2021-11-17 08:00:00'),
>>> time_max=Time('2021-11-17 10:00:00')
>>> )
>>> )
Deterministic algorithm
>>> schedule.insert(sun)
>>> schedule.book( optimize=False )
>>> schedule.plot()
>>> schedule.insert(sun * 2)
>>> schedule.book( optimize=False )
>>> schedule.plot()
>>> schedule.insert(sun * 3)
>>> schedule.book( optimize=False )
>>> schedule.plot()
2021-11-13 12:24:55 | INFO: Evaluating observation block constraints over the schedule...
2021-11-13 12:24:55 | INFO: 3 observation blocks have been successfully evaluated.
2021-11-13 12:24:55 | INFO: Fitting 3 observation blocks...
2021-11-13 12:24:55 | WARNING: <ObsBlock> #2 'Sun observation' cannot be scheduled.
2021-11-13 12:24:55 | INFO: 2/3 observation blocks scheduled (0 impossible to fit).
Genetic algorithm
>>> schedule.insert(sun * 3)
>>> schedule.book( optimize=True )
>>> schedule.plot()
>>> from astropy.coordinates import Angle
>>> schedule = Schedule(
>>> time_min=Time('2021-11-17 00:00:00'),
>>> time_max=Time('2021-11-18 00:00:00'),
>>> dt=TimeDelta(10*30, format='sec')
>>> )
>>> cas_a = ObsBlock(
>>> name="Cas A",
>>> program="ES10",
>>> target=ESTarget.fromName("Cas A"),
>>> duration=TimeDelta(2*3600, format='sec'),
>>> constraints=Constraints(LocalTimeCnst(
>>> hMin=Angle(18, unit="h"),
>>> hMax=Angle(9, unit="h"))
>>> )
>>> )
>>> ncp = ObsBlock(
>>> name="NCP",
>>> program="ES01",
>>> target=ESTarget.fromName("North Celestial Pole"),
>>> duration=TimeDelta(4*3600, format='sec'),
>>> constraints=Constraints(LocalTimeCnst(
>>> hMin=Angle(20, unit="h"),
>>> hMax=Angle(6, unit="h"))
>>> )
>>> )
>>> pulsar = ObsBlock(
>>> name="PSR J0953+0755",
>>> program='es03',
>>> target=ESTarget.fromName("PSR J0953+0755"),
>>> duration=TimeDelta(40*60, format='sec'),
>>> constraints = Constraints(
>>> ElevationCnst(0),
>>> TimeRangeCnst(
>>> time_min=Time('2021-11-17 05:00:00'),
>>> time_max=Time('2021-11-17 09:00:00')),
>>> MeridianTransitCnst())
>>> )
>>> pulsars = pulsar * 2
>>> mars = ObsBlock(
>>> name="Mars",
>>> program="ES06",
>>> target=SSTarget.fromName("Mars"),
>>> duration=TimeDelta(1.5*3600, format='sec'),
>>> constraints=Constraints( MeridianTransitCnst() )
>>> )
>>> jupiter = ObsBlock(
>>> name="Jupiter",
>>> program="ES07",
>>> target=SSTarget.fromName("Jupiter"),
>>> duration=TimeDelta(2*3600, format='sec'),
>>> constraints=Constraints( MeridianTransitCnst() )
>>> )
>>> sun = ObsBlock(
>>> name="Sun",
>>> program="ES11",
>>> target=SSTarget.fromName("Sun"),
>>> duration=TimeDelta(2*3600, format='sec'),
>>> constraints=Constraints( ElevationCnst(10), MeridianTransitCnst() )
>>> )
>>> cyg_x3 = ObsBlock(
>>> name="Cyg X-3",
>>> program="ES04",
>>> target=ESTarget.fromName("Cyg X-3"),
>>> duration=TimeDelta(1.5*3600, format='sec'),
>>> constraints=Constraints( MeridianTransitCnst() )
>>> )
>>> my_observations = cas_a + ncp + cyg_x3 + jupiter + mars + sun + pulsars
>>> schedule.insert(my_observations)
>>> ga = schedule.book(
>>> optimize=True, # Computes the booking using the GA algorithm
>>> population_size=100, # Number of individuals per generation
>>> generation_max=1000, # Stops the evolution after 1000 generations
>>> max_stagnating_generations=-1, # Stops the evolution if the best score has not increased over N gen.
>>> score_threshold=1., # Stops the evolution if the best score is above this value
>>> random_individuals=10, # Adds individuals with random genomes at each generation
>>> crossover='TPCO', # Defines the cross-over method
>>> selection='TNS' # Defines the parent selection method
>>> )
>>> schedule.plot(
>>> figsize=(10, 4),
>>> figname="/Users/aloh/Documents/GitHub/nenupy/docs/_images/obs_images/genetic_big.png"
>>> )
>>> ga.plot()
Schedule export
>>> schedule.export()
Table length=8
obsid name program start stop score
int64 str14 str4 object object float64
1 NCP es01 2021-11-17T01:00:00.000 2021-11-17T05:00:00.000 1.0
6 PSR J0953+0755 es03 2021-11-17T05:05:00.000 2021-11-17T05:45:00.000 0.666
7 PSR J0953+0755 es03 2021-11-17T05:45:00.000 2021-11-17T06:25:00.000 1.0
4 Mars es06 2021-11-17T09:55:00.000 2021-11-17T11:25:00.000 1.0
5 Sun es11 2021-11-17T11:25:00.000 2021-11-17T13:25:00.000 1.0
2 Cyg X-3 es04 2021-11-17T15:25:00.000 2021-11-17T16:55:00.000 1.0
3 Jupiter es07 2021-11-17T16:55:00.000 2021-11-17T18:55:00.000 1.0
0 Cas A es10 2021-11-17T18:55:00.000 2021-11-17T20:55:00.000 1.0