Source code for rubin_scheduler.scheduler.schedulers.filter_scheduler

__all__ = ("BandSwapScheduler", "SimpleBandSched", "ComCamBandSched", "BandSchedUzy", "DateSwapBandScheduler")

import numpy as np
from astropy.time import Time

from rubin_scheduler.scheduler.utils import IntRounded


# Dictionary keys based on astropy times make me nervous.
# Let's use the dayobs's string instead.
def time_to_key(time):
    return time.isot.split("T")[0]


def key_to_time(key):
    return Time(f"{key}T12:00:00", scale="tai")


[docs] class BandSwapScheduler: """A simple way to schedule what band to load""" def __init__(self): pass def add_observation(self, observation): pass
[docs] def __call__(self, conditions): """ Returns ------- list of strings for the bands that should be loaded """ pass
[docs] class SimpleBandSched(BandSwapScheduler): """Swap the mounted bands depending on the lunar phase. This assumes we swap between just two sets of bandpasses, one for lunar phases where moon illumination at sunset < `illum_limit` and another for phases > `illum_limit`. Parameters ---------- illum_limit The illumination limit to be compared to the `moon_illum` reported by the `Almanac`. """ def __init__(self, illum_limit=10.0): self.illum_limit_ir = IntRounded(illum_limit) def __call__(self, conditions): if IntRounded(conditions.moon_phase_sunset) > self.illum_limit_ir: result = ["g", "r", "i", "z", "y"] else: result = ["u", "g", "r", "i", "z"] return result
[docs] class ComCamBandSched(BandSwapScheduler): """ComCam can only hold 3 bands at a time. Pretend we will cycle from ugr, gri, riz, izy depending on lunar phase. Parameters ---------- loaded_band_groups : `tuple` (`tuple`) Groups of 3 bands, to be loaded at the same time. Multiple groups can be specified, to be swapped between at the boundaries of `illum_bins` in lunar phase. illum_bins : `np.ndarray`, (N,) Lunar illumination boundaries to define when to swap between different groups of bands within the `loaded_band_groups`. Lunar illumination ranges from 0 to 100. Notes ----- If illum_bins = np.array([0, 50, 100]), then there should be two groups of bands to use -- one for use between 0 and 50 percent illumination, and another for use between 50 and 100 percent illumination. """ def __init__( self, loaded_band_groups=(("u", "g", "r"), ("g", "r", "i"), ("r", "i", "z"), ("i", "z", "y")), illum_bins=np.arange(0, 100 + 1, 25), ): self.loaded_band_groups = loaded_band_groups self.illum_bins = illum_bins if isinstance(self.illum_bins, list): self.illum_bins = np.array(illum_bins) if len(illum_bins) - 1 > len(loaded_band_groups): raise ValueError("There are illumination bins with an " "undefined loaded_band_group") def __call__(self, conditions): moon_at_sunset = conditions.moon_phase_sunset try: if len(moon_at_sunset) > 0: moon_at_sunset = moon_at_sunset[0] except TypeError: pass indx = np.searchsorted(self.illum_bins, moon_at_sunset, side="left") indx = np.max([0, indx - 1]) result = list(self.loaded_band_groups[indx]) return result
[docs] class BandSchedUzy(BandSwapScheduler): """ remove u in bright time. Alternate between removing z and y in dark time. Note, this might not work properly if we need to restart a bunch. So a more robust way of scheduling band loading might be in order. """ def __init__(self, illum_limit=10.0): self.illum_limit_ir = IntRounded(illum_limit) self.last_swap = 0 self.bright_time = ["g", "r", "i", "z", "y"] self.dark_times = [["u", "g", "r", "i", "y"], ["u", "g", "r", "i", "z"]] def __call__(self, conditions): if IntRounded(conditions.moon_phase_sunset) > self.illum_limit_ir: result = self.bright_time else: indx = self.last_swap % 2 result = self.dark_times[indx] if result != conditions.mounted_bands: self.last_swap += 1 return result
[docs] class DateSwapBandScheduler(BandSwapScheduler): """Swap specific bands on specific days, up until end_date, then fall back to the backup_filter_scheduler. This provides a way for simulations to use the specific bands that were in use on specific nights, as well as to incorporate knowledge of the upcoming scheduler, while still falling back to a reasonable filter swap schedule beyond the time the dates are precisely known. Parameters ---------- swap_schedule Dictionary of times of the filter swaps, together with the filter complement loaded into the carousel. e.g. {"2025-08-20" : ['r', 'i', 'z', 'y']} end_date The Time after which the dictionary is no longer valid and the `backup_band_scheduler` should be used. backup_band_scheduler The more general band swap scheduler to use after the specific dates of the dictionary are exhausted. This should match the expected band scheduler for the Scheduler. """ def __init__( self, swap_schedule: dict[str, list[str]] | None = None, end_date: Time | None = None, backup_band_scheduler: BandSwapScheduler = SimpleBandSched(illum_limit=40), ): previous_swap_schedule = { "2025-06-20": ["u", "g", "r", "i", "z"], "2025-07-01": ["g", "r", "i", "z"], "2025-07-04": ["g", "r", "i", "z", "y"], "2025-07-10": ["z"], "2025-07-11": ["g", "r", "i", "z", "y"], "2025-07-15": ["u", "g", "r", "i", "z"], "2025-07-28": [ "u", "r", "i", "z", ], "2025-08-07": ["r", "i", "z", "y"], "2025-08-12": ["g", "r", "i", "z"], } previous_swap_times = np.sort(np.array([key_to_time(k) for k in previous_swap_schedule.keys()])) if swap_schedule is None: # Current estimate for the SV survey. # Subject to change, although past dates should match reality. swap_schedule = { "2025-08-12": ["g", "r", "i", "z"], } new_swap_times = np.sort(np.array([key_to_time(k) for k in swap_schedule.keys()])) # Join previous_swap_schedule and new schedule - # Use new swap_schedule if time overlaps with previous. keep_times = np.where(previous_swap_times < new_swap_times.min())[0] keep_keys = [time_to_key(t) for t in previous_swap_times[keep_times]] previous_swap_schedule = dict([(k, previous_swap_schedule[k]) for k in keep_keys]) self.swap_schedule = previous_swap_schedule self.swap_schedule.update(swap_schedule) self.swap_schedule_times = np.sort(np.array([key_to_time(k) for k in self.swap_schedule.keys()])) if end_date is None: self.end_date = Time("2025-09-25T12:00:00") else: self.end_date = end_date if backup_band_scheduler is None: self.backup_band_scheduler = SimpleBandSched(illum_limit=40) else: self.backup_band_scheduler = backup_band_scheduler def __call__(self, conditions): current_time = Time(conditions.mjd, format="mjd", scale="tai") # Are we within the bounds of the scheduled swaps? if current_time < self.end_date: idx = np.where(current_time >= self.swap_schedule_times)[0][-1] return self.swap_schedule[time_to_key(self.swap_schedule_times[idx])] else: return self.backup_band_scheduler(conditions)