Skip to content

Commit

Permalink
OPSIM-1196: more cleanup for summit use (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhiannonlynne authored Oct 3, 2024
2 parents 7389f73 + 2b29dd3 commit 60c7e44
Show file tree
Hide file tree
Showing 17 changed files with 460 additions and 453 deletions.
21 changes: 8 additions & 13 deletions rubin_scheduler/scheduler/basis_functions/basis_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ class AvoidFastRevisitsBasisFunction(BaseBasisFunction):
Parameters
----------
filtername: `str` or None
filtername : `str` or None
The name of the filter for this target map.
Using None will match visits in any filter.
gap_min : `float`
Expand Down Expand Up @@ -947,11 +947,6 @@ def __init__(self, max_time=135.0, filtername="r", nside=DEFAULT_NSIDE):
self.nside = nside
self.filtername = filtername

def add_observation(self, observation, indx=None):
# No tracking of observations in this basis function.
# Purely based on conditions.
pass

def _calc_value(self, conditions, indx=None):
# If we are in a different filter, the
# FilterChangeBasisFunction will take it
Expand Down Expand Up @@ -989,7 +984,7 @@ class CadenceEnhanceBasisFunction(BaseBasisFunction):
(days)
apply_area : healpix map
The area over which to try and drive the cadence.
Good values as 1, no candece drive 0.
Good values as 1, no cadence drive 0.
Probably works as a bool array too.
"""

Expand Down Expand Up @@ -1341,7 +1336,7 @@ class VisitGap(BaseBasisFunction):
Parameters
----------
note : str
note : `str`
Value of the observation "scheduler_note" field to be masked.
filter_names : list [str], optional
List of filter names that will be considered when evaluating
Expand Down Expand Up @@ -1369,11 +1364,11 @@ def __init__(self, note, filter_names=None, gap_min=25.0, penalty_val=np.nan):
self.survey_features = dict()
if self.filter_names is not None:
for filtername in self.filter_names:
self.survey_features[f"NoteLastObserved::{filtername}"] = features.NoteLastObserved(
self.survey_features[f"LastObservationMjd::{filtername}"] = features.LastObservationMjd(
note=note, filtername=filtername
)
else:
self.survey_features["NoteLastObserved"] = features.NoteLastObserved(note=note)
self.survey_features["LastObservationMjd"] = features.LastObservationMjd(scheduler_note=note)

def check_feasibility(self, conditions):
notes_last_observed = [last_observed.feature for last_observed in self.survey_features.values()]
Expand Down Expand Up @@ -1457,8 +1452,8 @@ def __init__(self, nobs_reference, note_survey, note_interest, nside=DEFAULT_NSI
self.nobs_reference = nobs_reference

self.survey_features = {}
self.survey_features["n_obs_survey"] = features.NObsCount(note=note_survey)
self.survey_features["n_obs_survey_interest"] = features.NObsCount(note=note_interest)
self.survey_features["n_obs_survey"] = features.NObsCount(scheduler_note=note_survey)
self.survey_features["n_obs_survey_interest"] = features.NObsCount(scheduler_note=note_interest)

def _calc_value(self, conditions, indx=None):
return (1 + np.floor(self.survey_features["n_obs_survey_interest"].feature / self.nobs_reference)) / (
Expand Down Expand Up @@ -1493,7 +1488,7 @@ def __init__(self, n_obs_survey, note_survey, nside=DEFAULT_NSIDE):
self.n_obs_survey = n_obs_survey

self.survey_features = {}
self.survey_features["n_obs_survey"] = features.NObsCount(note=note_survey)
self.survey_features["n_obs_survey"] = features.NObsCount(scheduler_note=note_survey)

def _calc_value(self, conditions, indx=None):
return self.survey_features["n_obs_survey"].feature % self.n_obs_survey
Expand Down
88 changes: 51 additions & 37 deletions rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ class AltAzShadowMaskBasisFunction(BaseBasisFunction):
regions which will enter the mask within `shadow_minutes`.
Masks any alt/az regions as specified by the conditions object in
`conditions.tel_az_limits` and `conditions.tel_alt_limits`,
`conditions.sky_az_limits` and `conditions.sky_alt_limits`,
as well as values as provided by `min_alt`, `max_alt`,
`min_az` and `max_az`.
Expand All @@ -197,6 +197,11 @@ class AltAzShadowMaskBasisFunction(BaseBasisFunction):
For sequences, try the length of the sequence + some buffer.
Note that this just sets up the shadow *at* the shadow_minutes time
(and not all times up to shadow_minutes).
altaz_limit_pad : `float`
The value by which to pad the telescope limits, to avoid
healpix values mapping into pointings from the field tesselations
which are actually out of bounds. This should typically be
a bit more than the radius of the fov. (degrees).
"""

def __init__(
Expand All @@ -207,13 +212,15 @@ def __init__(
min_az=0,
max_az=360,
shadow_minutes=40.0,
altaz_limit_pad=2.0,
):
super().__init__(nside=nside)
self.min_alt = np.radians(min_alt)
self.max_alt = np.radians(max_alt)
self.min_az = np.radians(min_az)
self.max_az = np.radians(max_az)
self.shadow_time = shadow_minutes / 60.0 / 24.0 # To days
self.altaz_limit_pad = np.radians(altaz_limit_pad)

self.r_min_alt = IntRounded(self.min_alt)
self.r_max_alt = IntRounded(self.max_alt)
Expand All @@ -235,29 +242,35 @@ def _calc_value(self, conditions, indx=None):
result[np.where(r_current_alt > self.r_max_alt)] = np.nan
result[np.where(r_future_alt < self.r_min_alt)] = np.nan
result[np.where(r_future_alt > self.r_max_alt)] = np.nan
# Check the conditions objects altitude limits, now and future
if (conditions.tel_alt_limits is not None) and (len(conditions.tel_alt_limits) > 0):

# Check the conditions objects 'sky_alt_limit', now and future
if (conditions.sky_alt_limits is not None) and (len(conditions.sky_alt_limits) > 0):
combined = np.zeros(hp.nside2npix(self.nside), dtype=float)
for limits in conditions.tel_alt_limits:
for limits in conditions.sky_alt_limits:
# For conditions-based limits, must add pad
# And remember that discontinuous areas can be allowed
in_bounds = np.ones(hp.nside2npix(self.nside), dtype=float)
min_alt = IntRounded(limits[0] + conditions.altaz_limit_pad)
max_alt = IntRounded(limits[1] - conditions.altaz_limit_pad)
min_alt = IntRounded(limits[0])
max_alt = IntRounded(limits[1])
in_bounds[np.where(r_current_alt < min_alt)] = 0
in_bounds[np.where(r_current_alt > max_alt)] = 0
in_bounds[np.where(r_future_alt < min_alt)] = 0
in_bounds[np.where(r_future_alt > max_alt)] = 0
combined += in_bounds
result[np.where(combined == 0)] = np.nan
# And check against the kinematic hard limits.
# These are separate so they don't override user tel_alt_limits.
min_alt = IntRounded(conditions.kinematic_alt_limits[0] + conditions.altaz_limit_pad)
max_alt = IntRounded(conditions.kinematic_alt_limits[1] - conditions.altaz_limit_pad)
result[np.where(r_current_alt < min_alt)] = np.nan
result[np.where(r_current_alt > max_alt)] = np.nan
result[np.where(r_future_alt < min_alt)] = np.nan
result[np.where(r_future_alt > max_alt)] = np.nan
# And check against the telescope 'tel_alt_limits'.
# The tel_alt_limits could be combined with the sky_alt_limits,
# but it's a bit tricky because sky_alt_limits are (potentially)
# disjoint allowable areas, while the tel_alt_limits are a single
# wide set of allowable area which explicitly disallows anything
# outside that range. The az limits versions are similar.
if conditions.tel_alt_limits is not None:
min_alt = IntRounded(conditions.tel_alt_limits[0])
max_alt = IntRounded(conditions.tel_alt_limits[1])
result[np.where(r_current_alt < min_alt)] = np.nan
result[np.where(r_current_alt > max_alt)] = np.nan
result[np.where(r_future_alt < min_alt)] = np.nan
result[np.where(r_future_alt > max_alt)] = np.nan

# note that % (mod) is not supported for IntRounded
two_pi = 2 * np.pi
Expand All @@ -269,39 +282,40 @@ def _calc_value(self, conditions, indx=None):
out_of_bounds = np.where((future_az - self.min_az) % (two_pi) > az_range)[0]
result[out_of_bounds] = np.nan
# Check the conditions objects azimuth limits, now and future
if (conditions.tel_az_limits is not None) and (len(conditions.tel_az_limits) > 0):
if (conditions.sky_az_limits is not None) and (len(conditions.sky_az_limits) > 0):
combined = np.zeros(hp.nside2npix(self.nside), dtype=float)
for limits in conditions.tel_az_limits:
for limits in conditions.sky_az_limits:
in_bounds = np.ones(hp.nside2npix(self.nside), dtype=float)
min_az = limits[0]
max_az = limits[1]
if np.abs(max_az - min_az) < two_pi:
az_range = (max_az - min_az) % (two_pi) - 2 * conditions.altaz_limit_pad
out_of_bounds = np.where(
(conditions.az - min_az - conditions.altaz_limit_pad) % (two_pi) > az_range
)[0]
az_range = (max_az - min_az) % (two_pi)
out_of_bounds = np.where((conditions.az - min_az) % (two_pi) > az_range)[0]
in_bounds[out_of_bounds] = 0
out_of_bounds = np.where(
(future_az - min_az - conditions.altaz_limit_pad) % (two_pi) > az_range
)[0]
out_of_bounds = np.where((future_az - min_az) % (two_pi) > az_range)[0]
in_bounds[out_of_bounds] = 0
combined += in_bounds
result[np.where(combined == 0)] = np.nan
# Check against the kinematic hard limits.
if np.abs(conditions.kinematic_az_limits[1] - conditions.kinematic_az_limits[0]) < two_pi:
az_range = (conditions.kinematic_az_limits[1] - conditions.kinematic_az_limits[0]) % (
two_pi
) - conditions.altaz_limit_pad * 2
out_of_bounds = np.where(
(conditions.az - conditions.kinematic_az_limits[0] - conditions.altaz_limit_pad) % (two_pi)
> az_range
)[0]
result[out_of_bounds] = np.nan
out_of_bounds = np.where(
(future_az - conditions.kinematic_az_limits[0] - conditions.altaz_limit_pad) % (two_pi)
> az_range
)[0]
result[out_of_bounds] = np.nan
if conditions.tel_az_limits is not None:
if np.abs(conditions.tel_az_limits[1] - conditions.tel_az_limits[0]) < two_pi:
az_range = (conditions.tel_az_limits[1] - conditions.tel_az_limits[0]) % (two_pi)
out_of_bounds = np.where((conditions.az - conditions.tel_az_limits[0]) % (two_pi) > az_range)[
0
]
result[out_of_bounds] = np.nan
out_of_bounds = np.where((future_az - conditions.tel_az_limits[0]) % (two_pi) > az_range)[0]
result[out_of_bounds] = np.nan

# Grow the resulting mask by self.altaz_limit_pad (approximately),
# to avoid pushing field centers
# outside the boundaries of what is actually reachable.
if self.altaz_limit_pad > 0:
# Can't smooth a map with nan
map_to_smooth = np.where(np.isnan(result), hp.UNSEEN, 1)
map_back = hp.smoothing(map_to_smooth, fwhm=self.altaz_limit_pad * 2)
# Replace values and nans
result = np.where(map_back < 0.9, np.nan, result)

return result

Expand Down
18 changes: 13 additions & 5 deletions rubin_scheduler/scheduler/example/simple_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@


def get_ideal_model_observatory(
nside: int = DEFAULT_NSIDE,
dayobs: str = "2024-09-09",
fwhm_500: float = 1.6,
wind_speed: float = 5.0,
Expand All @@ -48,6 +49,8 @@ def get_ideal_model_observatory(
Parameters
----------
nside : `int`
The nside for the model observatory.
dayobs : `str`
DAYOBS formatted str (YYYY-MM-DD) for the evening to start
up the observatory.
Expand Down Expand Up @@ -103,6 +106,7 @@ def get_ideal_model_observatory(

# Set up the model observatory
observatory = ModelObservatory(
nside=nside,
mjd=mjd_now,
mjd_start=survey_start,
kinem_model=kinematic_model, # Modified kinematics
Expand Down Expand Up @@ -669,12 +673,17 @@ def simple_greedy_survey(


def simple_rewards_field_survey(
nside: int = DEFAULT_NSIDE, sun_alt_limit: float = -12.0
scheduler_note: str,
nside: int = DEFAULT_NSIDE,
sun_alt_limit: float = -12.0,
) -> list[basis_functions.BaseBasisFunction]:
"""Get some simple rewards to observe a field survey for a long period.
Parameters
----------
scheduler_note : `str`
The scheduler note for the field survey.
Typically this will be the same as the field name.
nside : `int`
The nside value for the healpix grid.
sun_alt_limit : `float`, optional
Expand All @@ -687,8 +696,8 @@ def simple_rewards_field_survey(
"""
bfs = [
basis_functions.NotTwilightBasisFunction(sun_alt_limit=sun_alt_limit),
# Avoid revisits within 30 minutes
basis_functions.AvoidFastRevisitsBasisFunction(nside=nside, filtername=None, gap_min=30.0),
# Avoid revisits within 30 minutes - but we'll have to replace "note"
basis_functions.VisitGap(filter_names=None, note=scheduler_note, gap_min=30.0),
# reward fields which are rising, but don't mask out after zenith
basis_functions.RewardRisingBasisFunction(nside=nside, slope=0.1, penalty_val=0),
# Reward parts of the sky which are darker --
Expand Down Expand Up @@ -775,7 +784,7 @@ def simple_field_survey(
if mask_basis_functions is None:
mask_basis_functions = standard_masks(nside=nside)
if reward_basis_functions is None:
reward_basis_functions = simple_rewards_field_survey(nside=nside)
reward_basis_functions = simple_rewards_field_survey(field_name, nside=nside)
basis_functions = mask_basis_functions + reward_basis_functions

if nvisits is None:
Expand All @@ -794,7 +803,6 @@ def simple_field_survey(
exptimes=exptimes,
nexps=nexps,
ignore_obs=None,
accept_obs=[field_name],
survey_name=field_name,
scheduler_note=field_name,
target_name=field_name,
Expand Down
28 changes: 13 additions & 15 deletions rubin_scheduler/scheduler/features/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,19 @@ def __init__(
scheduled_observations : `np.ndarray`, (M,)
A list of MJD times when there are scheduled observations.
Defaults to empty array.
tel_az_limits : `list` [[`float`, `float`]]
sky_az_limits : `list` [[`float`, `float`]]
A list of lists giving valid azimuth ranges. e.g.,
[0, 2*np.pi] would mean all azimuth values are valid, while
[[0, np.pi/2], [3*np.pi/2, 2*np.pi]] would mean anywhere in
the south is invalid. Radians.
tel_alt_limits : `list` [[`float`, `float`]]
sky_alt_limits : `list` [[`float`, `float`]]
A list of lists giving valid altitude ranges. Radians.
altaz_limit_pad : `float`
Pad to surround the tel_az_limits and tel_alt_limits with.
tel_az_limits : `list` [`float`, `float`]
A simple two-element list giving the valid azimuth ranges
for the telescope movement. Radians.
tel_alt_limits : `list` [`float`, `float`]
A simple two-element list giving the valid altitude ranges
for the telescope movement. Radians
Attributes (calculated on demand and cached)
------------------------------------------
Expand Down Expand Up @@ -308,19 +312,13 @@ def _init_attributes(self):
self.tel_az = None
self.cumulative_azimuth_rad = None

# Telescope limits - these can be None
# Sky coverage limits.
# These should be in radians.
self.tel_az_limits = None
self.sky_az_limits = None
self.sky_alt_limits = None
# Kinematic model (real slew) limits.
self.tel_alt_limits = None
# Kinematic model (real slew) limits - these can't be None
# if you are using the AltAzShadowMaskBasisFunction.
# These generous limits won't restrict the AltAzShadowMask.
# Radians.
self.kinematic_alt_limits = [np.radians(-10), np.radians(100)]
self.kinematic_az_limits = [np.radians(-250), np.radians(250)]
# This has a (reasonable) default value, to avoid failure of
# AltAzShadowMask in case this isn't set otherwise.
self.altaz_limit_pad = np.radians(2)
self.tel_az_limits = None

# Full sky cloud map
self._cloud_map = None
Expand Down
Loading

0 comments on commit 60c7e44

Please sign in to comment.