Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OPSIM-1111: feature - Add altitude and azimuth limits to Conditions, along with new basis function + shadow mask #22

Merged
merged 4 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
"MaskAzimuthBasisFunction",
"SolarElongationMaskBasisFunction",
"AreaCheckMaskBasisFunction",
"AltAzShadowMaskBasisFunction",
)

import warnings

Check warning on line 15 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L15

Added line #L15 was not covered by tests

import healpy as hp
import matplotlib.pylab as plt
import numpy as np
Expand Down Expand Up @@ -194,6 +197,90 @@
return result


class AltAzShadowMaskBasisFunction(BaseBasisFunction):

Check warning on line 200 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L200

Added line #L200 was not covered by tests
"""Mask out range altitudes and azimuths, then extend the
mask so if observations are taken in pairs, the second in the pair will
not have moved into a masked region.

Masks any alt/az regions as specified by the conditions object, then
applies any additional altitude masking as suppied by the kwargs.
This mask is then extended using `shadow minutes`.

Parameters
----------
nside : `int`
HEALpix nside. Default None will look up the package-wide default.
min_alt : `float`
Minimum altitude to apply to the mask. Default 20 (degrees).
max_alt : `float`
Maximum altitude to allow. Default 82 (degrees).
shadow_minutes : `float`
How long to extend masked area in longitude. Default 40 (minutes).
"""
rhiannonlynne marked this conversation as resolved.
Show resolved Hide resolved

def __init__(

Check warning on line 221 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L221

Added line #L221 was not covered by tests
self,
nside=None,
min_alt=20.0,
max_alt=82.0,
shadow_minutes=40.0,
):
super(AltAzShadowMaskBasisFunction, self).__init__(nside=nside)
self.min_alt = np.radians(min_alt)
self.max_alt = np.radians(max_alt)
self.shadow_time = shadow_minutes / 60.0 / 24.0 # To days

Check warning on line 231 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L228-L231

Added lines #L228 - L231 were not covered by tests

def _calc_value(self, conditions, indx=None):

Check warning on line 233 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L233

Added line #L233 was not covered by tests
# Mask everything to start
result = np.zeros(hp.nside2npix(self.nside), dtype=float) + np.nan

Check warning on line 235 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L235

Added line #L235 was not covered by tests

in_range_alt = np.zeros(hp.nside2npix(self.nside), dtype=int)
in_range_az = np.zeros(hp.nside2npix(self.nside), dtype=int)

Check warning on line 238 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L237-L238

Added lines #L237 - L238 were not covered by tests

# Compute the alt,az values in the future. Use the conditions object
# so the results are cached and can be used by other surveys is needed.
# Technically this could fail if the masked region is very narrow or shadow time
# is very large.
future_alt, future_az = conditions.future_alt_az(np.max(conditions.mjd + self.shadow_time))

Check warning on line 244 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L244

Added line #L244 was not covered by tests

# apply limits from the conditions object
for limits in conditions.tel_alt_limits:
good = np.where(

Check warning on line 248 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L247-L248

Added lines #L247 - L248 were not covered by tests
(IntRounded(conditions.alt) >= IntRounded(np.min(limits)))
& (IntRounded(conditions.alt) <= IntRounded(np.max(limits)))
)[0]
in_range_alt[good] += 1
good = np.where(

Check warning on line 253 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L252-L253

Added lines #L252 - L253 were not covered by tests
(IntRounded(future_alt) >= IntRounded(np.min(limits)))
& (IntRounded(future_alt) <= IntRounded(np.max(limits)))
)[0]
in_range_alt[good] += 1

Check warning on line 257 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L257

Added line #L257 was not covered by tests

for limits in conditions.tel_az_limits:
good = np.where(

Check warning on line 260 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L259-L260

Added lines #L259 - L260 were not covered by tests
(IntRounded(conditions.az) >= IntRounded(np.min(limits)))
& (IntRounded(conditions.az) <= IntRounded(np.max(limits)))
)[0]
in_range_az[good] += 1
good = np.where(

Check warning on line 265 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L264-L265

Added lines #L264 - L265 were not covered by tests
(IntRounded(future_az) >= IntRounded(np.min(limits)))
& (IntRounded(future_az) <= IntRounded(np.max(limits)))
)[0]
in_range_az[good] += 1

Check warning on line 269 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L269

Added line #L269 was not covered by tests

passed_all = np.where((in_range_alt > 1) & (in_range_az > 1))[0]
result[passed_all] = 0

Check warning on line 272 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L271-L272

Added lines #L271 - L272 were not covered by tests

# Apply additional alt constraint in case we want to be more conservative than the limit
result[np.where(IntRounded(conditions.alt) < IntRounded(self.min_alt))] = np.nan
result[np.where(IntRounded(conditions.alt) > IntRounded(self.max_alt))] = np.nan

Check warning on line 276 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L275-L276

Added lines #L275 - L276 were not covered by tests

result[np.where(IntRounded(future_alt) < IntRounded(self.min_alt))] = np.nan
result[np.where(IntRounded(future_alt) > IntRounded(self.max_alt))] = np.nan

Check warning on line 279 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L278-L279

Added lines #L278 - L279 were not covered by tests

return result

Check warning on line 281 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L281

Added line #L281 was not covered by tests


class ZenithShadowMaskBasisFunction(BaseBasisFunction):
rhiannonlynne marked this conversation as resolved.
Show resolved Hide resolved
"""Mask the zenith, and things that will soon pass near zenith. Useful for making sure
observations will not be too close to zenith when they need to be observed again (e.g. for a pair).
Expand All @@ -217,6 +304,11 @@
penalty=np.nan,
site="LSST",
):
warnings.warn(

Check warning on line 307 in rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/basis_functions/mask_basis_funcs.py#L307

Added line #L307 was not covered by tests
"Deprecating ZenithShadowMaskBasisFunction in favor of AltAzShadowMaskBasisFunction.",
DeprecationWarning,
)

super(ZenithShadowMaskBasisFunction, self).__init__(nside=nside)
self.update_on_newobs = False

Expand Down
16 changes: 5 additions & 11 deletions rubin_scheduler/scheduler/example/example_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,12 +402,10 @@ def blob_for_long(
# Masks, give these 0 weight
bfs.append(
(
bf.ZenithShadowMaskBasisFunction(
bf.AltAzShadowMaskBasisFunction(
nside=nside,
shadow_minutes=shadow_minutes,
max_alt=max_alt,
penalty=np.nan,
site="LSST",
),
0.0,
)
Expand Down Expand Up @@ -606,7 +604,7 @@ def gen_greedy_surveys(
# Masks, give these 0 weight
bfs.append(
(
bf.ZenithShadowMaskBasisFunction(nside=nside, shadow_minutes=shadow_minutes, max_alt=max_alt),
bf.AltAzShadowMaskBasisFunction(nside=nside, shadow_minutes=shadow_minutes, max_alt=max_alt),
0,
)
)
Expand Down Expand Up @@ -824,12 +822,10 @@ def generate_blobs(
# Masks, give these 0 weight
bfs.append(
(
bf.ZenithShadowMaskBasisFunction(
bf.AltAzShadowMaskBasisFunction(
nside=nside,
shadow_minutes=shadow_minutes,
max_alt=max_alt,
penalty=np.nan,
site="LSST",
),
0.0,
)
Expand Down Expand Up @@ -1033,12 +1029,10 @@ def generate_twi_blobs(
# Masks, give these 0 weight
bfs.append(
(
bf.ZenithShadowMaskBasisFunction(
bf.AltAzShadowMaskBasisFunction(
nside=nside,
shadow_minutes=shadow_minutes,
max_alt=max_alt,
penalty=np.nan,
site="LSST",
),
0.0,
)
Expand Down Expand Up @@ -1232,7 +1226,7 @@ def generate_twilight_near_sun(
bfs.append((bf.NearSunTwilightBasisFunction(nside=nside, max_airmass=max_airmass), 0))
bfs.append(
(
bf.ZenithShadowMaskBasisFunction(nside=nside, shadow_minutes=shadow_minutes, max_alt=max_alt),
bf.AltAzShadowMaskBasisFunction(nside=nside, shadow_minutes=shadow_minutes, max_alt=max_alt),
0,
)
)
Expand Down
31 changes: 31 additions & 0 deletions rubin_scheduler/scheduler/features/conditions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
__all__ = ("Conditions",)

import functools

Check warning on line 3 in rubin_scheduler/scheduler/features/conditions.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/features/conditions.py#L3

Added line #L3 was not covered by tests
import warnings
from io import StringIO

Expand Down Expand Up @@ -169,6 +170,12 @@
Dictionary of planet name and coordinate e.g., 'venus_RA', 'mars_dec'
scheduled_observations : np.array
A list of MJD times when there are scheduled observations. Defaults to empty array.
tel_az_limits : list of float pairs
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 of float pairs
A list of lists giving valid altitude ranges. Radians.

Attributes (calculated on demand and cached)
------------------------------------------
Expand Down Expand Up @@ -283,6 +290,10 @@
self.tel_az = None
self.cumulative_azimuth_rad = None

# Telescope limits
self.tel_az_limits = None
self.tel_alt_limits = None

Check warning on line 295 in rubin_scheduler/scheduler/features/conditions.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/features/conditions.py#L294-L295

Added lines #L294 - L295 were not covered by tests

# Full sky cloud map
self._cloud_map = None
self._HA = None
Expand Down Expand Up @@ -387,6 +398,26 @@
self._mjd,
)

@functools.lru_cache(maxsize=10)
def future_alt_az(self, mjd):

Check warning on line 402 in rubin_scheduler/scheduler/features/conditions.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/features/conditions.py#L401-L402

Added lines #L401 - L402 were not covered by tests
"""Compute the altitude and azimuth for a future time.

Returns
-------
altitude : `np.array`
The altutude of each healpix at MJD (radians)
azimuth : : `np.array`
The azimuth of each healpix at MJD (radians)
"""
alt, az = _approx_ra_dec2_alt_az(

Check warning on line 412 in rubin_scheduler/scheduler/features/conditions.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/features/conditions.py#L412

Added line #L412 was not covered by tests
self.ra,
self.dec,
self.site.latitude_rad,
self.site.longitude_rad,
mjd,
)
return alt, az

Check warning on line 419 in rubin_scheduler/scheduler/features/conditions.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/features/conditions.py#L419

Added line #L419 was not covered by tests

@property
def mjd(self):
return self._mjd
Expand Down
10 changes: 10 additions & 0 deletions rubin_scheduler/scheduler/model_observatory/kinem_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@
azimuth_accel=7.0,
azimuth_decel=7.0,
settle_time=3.0,
az_limits=None,
alt_limits=None,
):
"""Parameters to define the TELESCOPE movement and position.

Expand Down Expand Up @@ -240,6 +242,14 @@
self.telaz_accel_rad = np.radians(azimuth_accel)
self.telaz_decel_rad = np.radians(azimuth_decel)
self.mount_settletime = settle_time
if alt_limits is None:
self.alt_limits = [[self.telalt_minpos_rad, self.telalt_maxpos_rad]]

Check warning on line 246 in rubin_scheduler/scheduler/model_observatory/kinem_model.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/model_observatory/kinem_model.py#L245-L246

Added lines #L245 - L246 were not covered by tests
else:
self.alt_limits = np.radians(alt_limits)
if az_limits is None:
self.az_limits = [[0, 2.0 * np.pi]]

Check warning on line 250 in rubin_scheduler/scheduler/model_observatory/kinem_model.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/model_observatory/kinem_model.py#L248-L250

Added lines #L248 - L250 were not covered by tests
else:
self.az_limits = np.radians(az_limits)

Check warning on line 252 in rubin_scheduler/scheduler/model_observatory/kinem_model.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/model_observatory/kinem_model.py#L252

Added line #L252 was not covered by tests

def setup_optics(self, ol_slope=1.0 / 3.5, cl_delay=[0.0, 36.0], cl_altlimit=[0.0, 9.0, 90.0]):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@

self.conditions.mjd_start = self.mjd_start

# Telescope limits
self.conditions.tel_az_limits = self.observatory.az_limits
self.conditions.tel_alt_limits = self.observatory.alt_limits

Check warning on line 371 in rubin_scheduler/scheduler/model_observatory/model_observatory.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/model_observatory/model_observatory.py#L370-L371

Added lines #L370 - L371 were not covered by tests

# Planet positions from almanac
self.conditions.planet_positions = self.almanac.get_planet_positions(self.mjd)

Expand Down
16 changes: 16 additions & 0 deletions rubin_scheduler/scheduler/surveys/scripted_surveys.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,22 @@
& ((HA > observation["HA_max"]) | (HA < observation["HA_min"]))
& (conditions.sun_alt < observation["sun_alt_max"])
)[0]

# Also check the alt,az limits given by the conditions object
count = in_range * 0
for limits in conditions.tel_alt_limits:
ir = np.where((alt[in_range] >= np.min(limits)) & (alt[in_range] <= np.max(limits)))[0]
count[ir] += 1
good = np.where(count > 0)[0]
in_range = in_range[good]

Check warning on line 203 in rubin_scheduler/scheduler/surveys/scripted_surveys.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/surveys/scripted_surveys.py#L198-L203

Added lines #L198 - L203 were not covered by tests

count = in_range * 0
for limits in conditions.tel_az_limits:
ir = np.where((az[in_range] >= np.min(limits)) & (az[in_range] <= np.max(limits)))[0]
count[ir] += 1
good = np.where(count > 0)[0]
in_range = in_range[good]

Check warning on line 210 in rubin_scheduler/scheduler/surveys/scripted_surveys.py

View check run for this annotation

Codecov / codecov/patch

rubin_scheduler/scheduler/surveys/scripted_surveys.py#L205-L210

Added lines #L205 - L210 were not covered by tests

return in_range

def _check_list(self, conditions):
Expand Down
4 changes: 2 additions & 2 deletions tests/scheduler/test_baseline.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def gen_greedy_surveys(nside):
bfs.append(bf.StrictFilterBasisFunction(filtername=filtername))
# Masks, give these 0 weight
bfs.append(bf.AvoidDirectWind(nside=nside))
bfs.append(bf.ZenithShadowMaskBasisFunction(nside=nside, shadow_minutes=60.0, max_alt=76.0))
bfs.append(bf.AltAzShadowMaskBasisFunction(nside=nside, shadow_minutes=60.0, max_alt=76.0))
bfs.append(bf.MoonAvoidanceBasisFunction(nside=nside, moon_distance=30.0))
bfs.append(bf.CloudedOutBasisFunction())

Expand Down Expand Up @@ -144,7 +144,7 @@ def gen_blob_surveys(nside):
bfs.append(bf.StrictFilterBasisFunction(filtername=filtername))
# Masks, give these 0 weight
bfs.append(bf.AvoidDirectWind(nside=nside))
bfs.append(bf.ZenithShadowMaskBasisFunction(nside=nside, shadow_minutes=60.0, max_alt=76.0))
bfs.append(bf.AltAzShadowMaskBasisFunction(nside=nside, shadow_minutes=60.0, max_alt=76.0))
bfs.append(bf.MoonAvoidanceBasisFunction(nside=nside, moon_distance=30.0))
bfs.append(bf.CloudedOutBasisFunction())
# feasibility basis fucntions. Also give zero weight.
Expand Down
45 changes: 43 additions & 2 deletions tests/scheduler/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from rubin_scheduler.data import get_data_dir
from rubin_scheduler.scheduler import sim_runner
from rubin_scheduler.scheduler.example import example_scheduler, run_sched
from rubin_scheduler.scheduler.model_observatory import ModelObservatory
from rubin_scheduler.scheduler.model_observatory import KinemModel, ModelObservatory
from rubin_scheduler.scheduler.utils import restore_scheduler, run_info_table, season_calc
from rubin_scheduler.utils import survey_start_mjd

Expand All @@ -30,7 +30,7 @@ def test_example(self):
"""Test the example scheduler executes all the expected surveys"""
mjd_start = survey_start_mjd()
scheduler = example_scheduler(mjd_start=mjd_start)
observatory, scheduler, observations = run_sched(scheduler, mjd_start=mjd_start, survey_length=5)
observatory, scheduler, observations = run_sched(scheduler, mjd_start=mjd_start, survey_length=7)
u_notes = np.unique(observations["note"])

# Note that some of these may change and need to be updated if survey
Expand Down Expand Up @@ -84,6 +84,47 @@ def test_start_of_night_example(self):
os.path.isfile(os.path.join(get_data_dir(), "scheduler/dust_maps/dust_nside_32.npz")),
"Test data not available.",
)
def test_altaz_limit(self):
"""Test that setting some azimuth limits via the kinematic model works"""
mjd_start = survey_start_mjd()
scheduler = example_scheduler(mjd_start=mjd_start)
km = KinemModel(mjd0=mjd_start)
km.setup_telescope(az_limits=[[0.0, 90.0], [270.0, 360.0]])
mo = ModelObservatory(mjd_start=mjd_start, kinem_model=km)

mo, scheduler, observations = sim_runner(
mo,
scheduler,
survey_length=3.0,
verbose=False,
filename=None,
)

az = np.degrees(observations["az"])
forbidden = np.where((az > 90) & (az < 270))[0]
# Let a few pairs try to complete since by default we don't use an agressive shadow_minutes
n_forbidden = np.size(
[obs for obs in observations[forbidden]["note"] if (("pair_33" not in obs) | (", b" not in obs))]
)

assert n_forbidden == 0

km = KinemModel(mjd0=mjd_start)
km.setup_telescope(alt_limits=[[40.0, 70.0]])
mo = ModelObservatory(mjd_start=mjd_start, kinem_model=km)

mo, scheduler, observations = sim_runner(
mo,
scheduler,
survey_length=3.0,
verbose=False,
filename=None,
)
alt = np.degrees(observations["alt"])
n_forbidden = np.size(np.where((alt > 70) & (alt < 40))[0])

assert n_forbidden == 0

def test_restore(self):
"""Test we can restore a scheduler properly"""
# MJD set so it's in test data range
Expand Down
Loading