diff --git a/rubin_scheduler/scheduler/example/__init__.py b/rubin_scheduler/scheduler/example/__init__.py index eb803c6e..47495709 100644 --- a/rubin_scheduler/scheduler/example/__init__.py +++ b/rubin_scheduler/scheduler/example/__init__.py @@ -1,2 +1,2 @@ -from .comcam_sv_scheduler_example import * from .example_scheduler import * +from .simple_examples import * diff --git a/rubin_scheduler/scheduler/example/comcam_sv_scheduler_example.py b/rubin_scheduler/scheduler/example/simple_examples.py similarity index 76% rename from rubin_scheduler/scheduler/example/comcam_sv_scheduler_example.py rename to rubin_scheduler/scheduler/example/simple_examples.py index b6ca46c3..900b827d 100644 --- a/rubin_scheduler/scheduler/example/comcam_sv_scheduler_example.py +++ b/rubin_scheduler/scheduler/example/simple_examples.py @@ -1,44 +1,40 @@ __all__ = ( - "get_model_observatory", + "get_ideal_model_observatory", "update_model_observatory_sunset", "standard_masks", "simple_rewards", "simple_pairs_survey", "simple_greedy_survey", - "get_basis_functions_field_survey", - "get_field_survey", - "get_sv_fields", - "prioritize_fields", - "get_comcam_sv_schedulers", + "simple_rewards_field_survey", + "simple_field_survey", ) -import copy import numpy as np from astropy.time import Time import rubin_scheduler.scheduler.basis_functions as basis_functions import rubin_scheduler.scheduler.detailers as detailers -from rubin_scheduler.scheduler.detailers import CameraSmallRotPerObservationListDetailer +import rubin_scheduler.scheduler.features as features from rubin_scheduler.scheduler.model_observatory import ( KinemModel, ModelObservatory, rotator_movement, tma_movement, ) -from rubin_scheduler.scheduler.schedulers import ComCamFilterSched, CoreScheduler, FilterSwapScheduler +from rubin_scheduler.scheduler.schedulers import FilterSwapScheduler from rubin_scheduler.scheduler.surveys import BlobSurvey, FieldSurvey, GreedySurvey from rubin_scheduler.scheduler.utils import Footprint, get_current_footprint from rubin_scheduler.site_models import Almanac, ConstantSeeingData, ConstantWindData from rubin_scheduler.utils import survey_start_mjd -def get_model_observatory( +def get_ideal_model_observatory( dayobs: str = "2024-09-09", - fwhm_500: float = 2.0, + fwhm_500: float = 1.6, wind_speed: float = 5.0, wind_direction: float = 340, - tma_percent: float = 10, + tma_percent: float = 70, rotator_percent: float = 100, survey_start: Time = Time("2024-09-09T12:00:00", format="isot", scale="utc").mjd, ) -> ModelObservatory: @@ -58,7 +54,7 @@ def get_model_observatory( fwhm_500 : `float`, optional The value to set for atmospheric component of seeing, constant seeing throughout the night (arcseconds). - Ad-hoc default for start of comcam on-sky operations about 2.0". + Ad-hoc value for start of comcam on-sky operations about 2.0". wind_speed : `float`, optional Set a (constant) wind speed for the night, (m/s). Default of 5.0 is minimal but noticeable. @@ -68,7 +64,7 @@ def get_model_observatory( the site (see SITCOMTN-126). tma_percent : `float`, optional Set a percent of full-performance for the telescope TMA (0-100). - Default of 10(%) is likely for start of comcam on-sky SV surveys. + Value of 10(%) is likely for start of comcam on-sky SV surveys. rotator_percent : `float`, optional Set a percent of full-performance for the rotator. Default of 100% is likely for the start of comcam on-sky SV surveys. @@ -123,8 +119,7 @@ def get_model_observatory( def update_model_observatory_sunset( observatory: ModelObservatory, filter_scheduler: FilterSwapScheduler, twilight: int | float = -12 ) -> ModelObservatory: - """Move model observatory to twilight and ensure correct filters are in - place according to the filter_scheduler. + """Ensure correct filters are in place according to the filter_scheduler. Parameters ---------- @@ -346,6 +341,7 @@ def simple_pairs_survey( pair_time: float = 30.0, exptime: float = 30.0, nexp: int = 1, + science_program: str | None = None, ) -> BlobSurvey: """Set up a simple blob survey to acquire pairs of visits. @@ -386,6 +382,9 @@ def simple_pairs_survey( The on-sky exposure time per visit. nexp : `int` The number of exposures per visit (exptime * nexp = total on-sky time). + science_program : `str` | None + The science_program key for the FieldSurvey. + This maps to the BLOCK and `science_program` in the consDB. Returns ------- @@ -473,9 +472,9 @@ def simple_pairs_survey( # Set up blob surveys. if filtername2 is None: - scheduler_note = "pair_%i, %s" % (pair_time, filtername) + survey_name = "simple pair %i, %s" % (pair_time, filtername) else: - scheduler_note = "pair_%i, %s%s" % (pair_time, filtername, filtername2) + survey_name = "simple pair %i, %s%s" % (pair_time, filtername, filtername2) # Set up detailers for each requested observation. detailer_list = [] @@ -500,7 +499,6 @@ def simple_pairs_survey( "slew_approx": 7.5, "filter_change_approx": 140.0, "read_approx": 2.4, - "search_radius": 30.0, "flush_time": pair_time * 3, "smoothing_kernel": None, "nside": nside, @@ -516,13 +514,19 @@ def simple_pairs_survey( filtername2=filtername2, exptime=exptime, ideal_pair_time=pair_time, - scheduler_note=scheduler_note, + survey_name=survey_name, ignore_obs=ignore_obs, nexp=nexp, detailers=detailer_list, + science_program=science_program, **BlobSurvey_params, ) + # Tucking this here so we can look at how many observations + # recorded for this survey and what was the last one. + pair_survey.extra_features["ObsRecorded"] = features.NObsSurvey() + pair_survey.extra_features["LastObs"] = features.LastObservation() + return pair_survey @@ -537,6 +541,7 @@ def simple_greedy_survey( camera_rot_limits: list[float] = [-80.0, 80.0], exptime: float = 30.0, nexp: int = 1, + science_program: str | None = None, ) -> GreedySurvey: """Set up a simple greedy survey to just observe single visits. @@ -572,6 +577,9 @@ def simple_greedy_survey( The on-sky exposure time per visit. nexp : `int` The number of exposures per visit (exptime * nexp = total on-sky time). + science_program : `str` | None + The science_program key for the FieldSurvey. + This maps to the BLOCK and `science_program` in the consDB. Returns ------- @@ -624,8 +632,8 @@ def simple_greedy_survey( reward_basis_functions_weights = [val[1] for val in reward_functions] reward_basis_functions = [val[0] for val in reward_functions] - # Set up scheduler note. - scheduler_note = f"greedy {filtername}" + # Set up survey name, use also for scheduler note. + survey_name = f"simple greedy {filtername}" # Set up detailers for each requested observation. detailer_list = [] @@ -654,71 +662,40 @@ def simple_greedy_survey( reward_basis_functions_weights + mask_basis_functions_weights, filtername=filtername, exptime=exptime, - scheduler_note=scheduler_note, + survey_name=survey_name, ignore_obs=ignore_obs, nexp=nexp, detailers=detailer_list, + science_program=science_program, **GreedySurvey_params, ) + # Tucking this here so we can look at how many observations + # recorded for this survey and what was the last one. + greedy_survey.extra_features["ObsRecorded"] = features.NObsSurvey() + greedy_survey.extra_features["LastObs"] = features.LastObservation() + return greedy_survey -def get_basis_functions_field_survey( - nside: int = 32, - wind_speed_maximum: float = 10, - moon_distance: float = 30, - min_alt: float = 20, - max_alt: float = 86, - min_az: float = 0, - max_az: float = 360, - shadow_minutes: float = 60, +def simple_rewards_field_survey( + nside: int = 32, sun_alt_limit: float = -12.0 ) -> list[basis_functions.BaseBasisFunction]: - """Get the basis functions for a comcam SV field survey. + """Get some simple rewards to observe a field survey for a long period. Parameters ---------- nside : `int` The nside value for the healpix grid. - wind_speed_maximum : `float` - Maximum wind speed tolerated for the observations of the survey, - in m/s. - moon_distance : `float`, optional - Moon avoidance distance, in degrees. - min_alt : `float`, optional - Minimum altitude (in degrees) to observe. - max_alt : `float`, optional - Maximum altitude (in degrees) to observe. - min_az : `float`, optional - Minimum azimuth angle (in degrees) to observe. - max_az : `float`, optional - Maximum azimuth angle (in degrees) to observe. - shadow_minutes : `float`, optional - Avoid inaccessible alt/az regions, as well as parts of the sky - which will move into those regions within `shadow_minutes` (minutes). - For the FieldSurvey, this should probably be the length of time - required by the sequence in the FieldSurvey, to avoid tracking into - inaccessible areas of sky. + sun_alt_limit : `float`, optional + Value for the sun's altitude at which to allow observations to start + (or finish). Returns ------- bfs : `list` of `~.scheduler.basis_functions.BaseBasisFunction` """ - sun_alt_limit = -12.0 - moon_distance = 30 - - bfs = standard_masks( - nside=nside, - moon_distance=moon_distance, - wind_speed_maximum=wind_speed_maximum, - min_alt=min_alt, - max_alt=max_alt, - min_az=min_az, - max_az=max_az, - shadow_minutes=shadow_minutes, - ) - - bfs += [ + 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), @@ -733,15 +710,21 @@ def get_basis_functions_field_survey( return bfs -def get_field_survey( +def simple_field_survey( field_ra_deg: float, field_dec_deg: float, field_name: str, - basis_functions: list[basis_functions.BaseBasisFunction], - detailers: list[detailers.BaseDetailer], + mask_basis_functions: list[basis_functions.BaseBasisFunction] | None = None, + reward_basis_functions: list[basis_functions.BaseBasisFunction] | None = None, + detailers: list[detailers.BaseDetailer] | None = None, + sequence: str | list[str] = "ugrizy", + nvisits: dict | None = None, + exptimes: dict | None = None, + nexps: dict | None = None, nside: int = 32, + science_program: str | None = None, ) -> FieldSurvey: - """Set up a comcam SV field survey. + """Set up a simple field survey. Parameters ---------- @@ -754,15 +737,36 @@ def get_field_survey( transferred to the 'target' information in the output observation. Also used in 'scheduler_note', which is important for the FieldSurvey to know whether to count particular observations for the Survey. - basis_functions : `list` of [`~.scheduler.basis_function` objects] - Basis functions for the field survey. - A default set can be obtained from `get_basis_functions_field_survey`. + mask_basis_functions : `list` [`BaseBasisFunction`] or None + List of basis functions to use as masks (with implied weight 0). + If None, `standard_masks` is used with default parameters. + reward_basis_functions : `list` [`BaseBasisFunction`] or None + List of basis functions to use as rewards. + If None, a basic set of basis functions useful for long observations + of a field within a night will be used (`get detailers : `list` of [`~.scheduler.detailer` objects] Detailers for the survey. Detailers can add information to output observations, including specifying rotator or dither positions. + sequence : `str` or `list` [`str`] + The filters (in order?) for the sequence of observations. + nvisits : `dict` {`str`:`int`} | None + Number of visits per filter to program in the sequence. + Default of None uses + nvisits = {"u": 20, "g": 20, "r": 20, "i": 20, "z": 20, "y": 20} + exptimes : `dict` {`str`: `float`} | None + Exposure times per filter to program in the sequence. + Default of None uses + exptimes = {"u": 38, "g": 30, "r": 30, "i": 30, "z": 30, "y": 30} + nexps : `dict` {`str`: `int`} | None + Number of exposures per filter to program in the sequence. + Default of None uses + nexps = {"u": 1, "g": 2, "r": 2, "i": 2, "z": 2, "y": 2} nside : `int`, optional Nside for the survey. Default 32. + science_program : `str` | None + The science_program key for the FieldSurvey. + This maps to the BLOCK and `science_program` in the consDB. Returns ------- @@ -778,159 +782,37 @@ def get_field_survey( field_survey.extra_features['ObsRecord'] tracks how many observations have been accepted by the Survey (and can be useful for diagnostics). """ + 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) + basis_functions = mask_basis_functions + reward_basis_functions + + if nvisits is None: + nvisits = {"u": 20, "g": 20, "r": 20, "i": 20, "z": 20, "y": 20} + if exptimes is None: + exptimes = {"u": 38, "g": 30, "r": 30, "i": 30, "z": 30, "y": 30} + if nexps is None: + nexps = {"u": 1, "g": 2, "r": 2, "i": 2, "z": 2, "y": 2} + field_survey = FieldSurvey( basis_functions, field_ra_deg, field_dec_deg, - sequence="ugrizy", - nvisits={"u": 20, "g": 20, "r": 20, "i": 20, "z": 20, "y": 20}, - exptimes={"u": 38, "g": 30, "r": 30, "i": 30, "z": 30, "y": 30}, - nexps={"u": 1, "g": 2, "r": 2, "i": 2, "z": 2, "y": 2}, + sequence=sequence, + nvisits=nvisits, + exptimes=exptimes, + nexps=nexps, ignore_obs=None, accept_obs=[field_name], survey_name=field_name, - scheduler_note=None, + scheduler_note=field_name, + target_name=field_name, readtime=2.4, filter_change_time=120.0, nside=nside, flush_pad=30.0, detailers=detailers, + science_program=science_program, ) return field_survey - - -def get_sv_fields() -> dict[str, dict[str, float]]: - """Default potential fields for the SV surveys. - - Returns - ------- - fields_dict : `dict` {`str` : {'RA' : `float`, 'Dec' : `float`}} - A dictionary keyed by field_name, containing RA and Dec (in degrees) - for each field. - """ - fields = ( - ("Rubin_SV_095_-25", 95.0, -25.0), # High stellar densty, low extinction - ("Rubin_SV_125_-15", 125.0, -15.0), # High stellar densty, low extinction - ("DESI_SV3_R1", 179.60, 0.000), # DESI, GAMA, HSC DR2, KiDS-N - ("Rubin_SV_225_-40", 225.0, -40.0), # 225 High stellar densty, low extinction - ("DEEP_A0", 216, -12.5), # DEEP Solar Systen - ("Rubin_SV_250_2", 250.0, 2.0), # 250 High stellar densty, low extinction - ("Rubin_SV_300_-41", 300.0, -41.0), # High stellar densty, low extinction - ("Rubin_SV_280_-48", 280.0, -48.0), # High stellar densty, low extinction - ("DEEP_B0", 310, -19), # DEEP Solar System - ("ELAIS_S1", 9.45, -44.03), # ELAIS-S1 LSST DDF - ("XMM_LSS", 35.575, -4.82), # LSST DDF - ("ECDFS", 52.98, -28.1), # ECDFS - ("COSMOS", 150.1, 2.23), # COSMOS - ("EDFS_A", 58.9, -49.32), # EDFS_a - ("EDFS_B", 63.6, -47.6), # EDFS_b - ) - - fields_dict = dict(zip([f[0] for f in fields], [{"RA": f[1], "Dec": f[2]} for f in fields])) - - return fields_dict - - -def prioritize_fields( - priority_fields: list[str] | None = None, field_dict: dict[str, dict[str, float]] | None = None -) -> list[list[FieldSurvey]]: - """Add the remaining field names in field_dict into the last - tier of 'priority_fields' field names, creating a complete - survey tier list of lists. - - Parameters - ---------- - priority_fields : `list` [`list`] - A list of lists, where each final list corresponds to a 'tier' - of FieldSurveys, and contains those field survey names. - These names must be present in field_dict. - field_dict : `dict` {`str` : {'RA' : `float`, 'Dec' : `float`}} or None - Dictionary containing field information for the FieldSurveys. - Default None will fetch the SV fields from 'get_sv_fields'. - - Returns - ------- - tiers : `list` [`list`] - The tiers to pass to the core scheduler, after including the - non-prioritized fields from field_dict. - """ - if field_dict is None: - field_dict = get_sv_fields() - else: - field_dict = copy.deepcopy(field_dict) - tiers = [] - if priority_fields is not None: - for tier in priority_fields: - tiers.append(tier) - for field in tier: - del field_dict[field] - remaining_fields = list(field_dict.keys()) - tiers.append(remaining_fields) - return tiers - - -def get_comcam_sv_schedulers( - starting_tier: int = 0, - tiers: list[list[str]] | None = None, - field_dict: dict[str, dict[str, float]] | None = None, - nside: int = 32, -) -> (CoreScheduler, ComCamFilterSched): - """Set up a CoreScheduler and FilterScheduler generally - appropriate for ComCam SV observing. - - Parameters - ---------- - starting_tier : `int`, optional - Starting to tier to place the surveys coming from the 'tiers' - specified here. - Default 0, to start at first tier. If an additional - survey will be added at highest tier after (such as cwfs), then - set starting tier to 1+ and add these surveys as a list to - scheduler.survey_lists[tier] etc. - tiers : `list` [`str`] or None - Field names for each of the field surveys in tiers. - Should be a list of lists - [tier][surveys_in_tier] - [[field1, field2],[field3, field4, field5]. - Fields should be present in the 'field_dict'. - Default None will use all fields in field_dict. - field_dict : `dict` {`str` : {'RA' : `float`, 'Dec' : `float`}} or None - Dictionary containing field information for the FieldSurveys. - Default None will fetch the SV fields from 'get_sv_fields'. - nside : `int` - Nside for the scheduler. Default 32. - Generally, don't change without serious consideration. - - Returns - ------- - scheduler, filter_scheduler : `~.scheduler.schedulers.CoreScheduler`, - `~.scheduler.schedulers.ComCamFilterSched` - A CoreScheduler and FilterScheduler that are generally - appropriate for ComCam. - """ - if field_dict is None: - field_dict = get_sv_fields() - - if tiers is None: - tiers = [list(field_dict.keys())] - - surveys = [] - - i = 0 - for t in tiers: - if len(t) == 0: - continue - j = i + starting_tier - i += 1 - surveys.append([]) - for kk, fieldname in enumerate(t): - bfs = get_basis_functions_field_survey() - detailer = CameraSmallRotPerObservationListDetailer(per_visit_rot=0.5) - surveys[j].append( - get_field_survey( - field_dict[fieldname]["RA"], field_dict[fieldname]["Dec"], fieldname, bfs, [detailer] - ) - ) - - scheduler = CoreScheduler(surveys, nside=nside) - filter_scheduler = ComCamFilterSched() - return scheduler, filter_scheduler diff --git a/rubin_scheduler/scheduler/surveys/field_survey.py b/rubin_scheduler/scheduler/surveys/field_survey.py index 35ed315d..ad31d689 100644 --- a/rubin_scheduler/scheduler/surveys/field_survey.py +++ b/rubin_scheduler/scheduler/surveys/field_survey.py @@ -246,7 +246,7 @@ def add_observation(self, observation, **kwargs): passed_accept = True if passed_ignore and self.accept_obs is not None: # Check if this observation matches any accept string. - checks = [io == str(observation["scheduler_note"]) for io in self.accept_obs] + checks = [io == str(observation["scheduler_note"][0]) for io in self.accept_obs] passed_accept = any(checks) # I think here I have to assume observation is an # array and not a dict. diff --git a/rubin_scheduler/scheduler/surveys/surveys.py b/rubin_scheduler/scheduler/surveys/surveys.py index 16c7e8df..787f4ce7 100644 --- a/rubin_scheduler/scheduler/surveys/surveys.py +++ b/rubin_scheduler/scheduler/surveys/surveys.py @@ -156,6 +156,14 @@ class BlobSurvey(GreedySurvey): the reward function. (degrees) Note that traveling salesman solver can have rare failures if this is set too large (probably issue with projection effects or something). + + Notes + ----- + The `scheduler_note` for the BlobSurvey will be set from the + `survey_name`. A typical Detailer for the blob survey + then adds onto this note to identify the first vs. second visit of + the pair. Because the `scheduler_note` is modified, users do not set + `scheduler_note` directly. """ def __init__( diff --git a/tests/scheduler/test_comcam_surveys.py b/tests/scheduler/test_comcam_surveys.py deleted file mode 100644 index 9cd17978..00000000 --- a/tests/scheduler/test_comcam_surveys.py +++ /dev/null @@ -1,76 +0,0 @@ -import unittest - -import numpy as np -from astropy.time import Time - -from rubin_scheduler.scheduler import sim_runner -from rubin_scheduler.scheduler.example import ( - get_comcam_sv_schedulers, - get_model_observatory, - get_sv_fields, - update_model_observatory_sunset, -) -from rubin_scheduler.scheduler.schedulers import ComCamFilterSched -from rubin_scheduler.utils import survey_start_mjd - - -class TestComCamSurveys(unittest.TestCase): - - def test_model_observatory_conveniences(self): - """Test the model observatory convenience functions.""" - - # Just check that we can acquire a model observatory - # and it is set up for the date expected. - survey_start = survey_start_mjd() - survey_start = np.floor(survey_start) + 0.5 - dayobs = Time(survey_start, format="mjd", scale="utc").iso[:10] - observatory = get_model_observatory(dayobs=dayobs, survey_start=survey_start) - conditions = observatory.return_conditions() - assert conditions.mjd == observatory.mjd - # The model observatory automatically advanced to -12 deg sunset - assert (conditions.mjd - survey_start) < 1 - sun_ra_start = conditions.sun_ra_start - mjd_start = observatory.mjd_start - - newday = survey_start + 4 - new_dayobs = Time(newday, format="mjd", scale="utc").iso[:10] - newday = Time(f"{new_dayobs}T12:00:00", format="isot", scale="utc").mjd - observatory = get_model_observatory(dayobs=new_dayobs, survey_start=survey_start) - conditions = observatory.return_conditions() - assert (conditions.mjd - newday) < 1 - # Check that advancing the day did not change the expected location - # of the sun at the *start* of the survey - assert conditions.mjd_start == mjd_start - assert conditions.sun_ra_start == sun_ra_start - - # And update observatory to sunset, using a filter scheduler - # that only has 'g' available - filter_sched = ComCamFilterSched(illum_bins=np.arange(0, 101, 100), loaded_filter_groups=(("g",))) - observatory = update_model_observatory_sunset(observatory, filter_sched, twilight=-18) - assert observatory.observatory.current_filter == "g" - assert observatory.conditions.sun_alt < np.radians(18) - - def test_comcam_sv_sched(self): - """Test the comcam sv survey scheduler setup.""" - # This is likely to change as we go into commissioning, - # so mostly I'm just going to test that the end result is - # a usable scheduler - survey_start = survey_start_mjd() - survey_start = np.floor(survey_start) + 0.5 - dayobs = Time(survey_start, format="mjd", scale="utc").iso[:10] - scheduler, filter_scheduler = get_comcam_sv_schedulers() - observatory = get_model_observatory(dayobs=dayobs, survey_start=survey_start) - observatory = update_model_observatory_sunset(observatory, filter_scheduler) - - observatory, scheduler, observations = sim_runner( - observatory, scheduler, filter_scheduler, survey_length=30 - ) - assert len(observations) > 24000 - sv_fields = set(np.unique(observations["scheduler_note"])) - all_sv_fields = set(list(get_sv_fields().keys())) - # Probably won't observe all of the fields, but can do some. - assert sv_fields.intersection(all_sv_fields) == sv_fields - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/scheduler/test_simple_examples.py b/tests/scheduler/test_simple_examples.py new file mode 100644 index 00000000..f1968747 --- /dev/null +++ b/tests/scheduler/test_simple_examples.py @@ -0,0 +1,145 @@ +import unittest + +import numpy as np +from astropy.time import Time + +from rubin_scheduler.scheduler import sim_runner +from rubin_scheduler.scheduler.example import ( + get_ideal_model_observatory, + simple_field_survey, + simple_greedy_survey, + simple_pairs_survey, + update_model_observatory_sunset, +) +from rubin_scheduler.scheduler.schedulers import ComCamFilterSched, CoreScheduler +from rubin_scheduler.utils import survey_start_mjd + + +class TestSurveyConveniences(unittest.TestCase): + + def setUp(self) -> None: + self.survey_start = np.floor(survey_start_mjd()) + 0.5 + self.day_obs_start = Time(self.survey_start, format="mjd", scale="utc").iso[:10] + + def test_model_observatory_conveniences(self): + """Test the model observatory convenience functions.""" + + # Just check that we can acquire a model observatory + # and it is set up for the date expected. + observatory = get_ideal_model_observatory(dayobs=self.day_obs_start, survey_start=self.survey_start) + conditions = observatory.return_conditions() + assert conditions.mjd == observatory.mjd + # The model observatory automatically advanced to -12 deg sunset + assert (conditions.mjd - self.survey_start) < 1 + sun_ra_start = conditions.sun_ra_start + mjd_start = observatory.mjd_start + + newday = self.survey_start + 4 + new_dayobs = Time(newday, format="mjd", scale="utc").iso[:10] + newday = Time(f"{new_dayobs}T12:00:00", format="isot", scale="utc").mjd + observatory = get_ideal_model_observatory(dayobs=new_dayobs, survey_start=self.survey_start) + conditions = observatory.return_conditions() + assert (conditions.mjd - newday) < 1 + # Check that advancing the day did not change the expected location + # of the sun at the *start* of the survey + assert conditions.mjd_start == mjd_start + assert conditions.sun_ra_start == sun_ra_start + + # And update observatory to sunset, using a filter scheduler + # that only has 'g' available + filter_sched = ComCamFilterSched(illum_bins=np.arange(0, 101, 100), loaded_filter_groups=(("g",))) + observatory = update_model_observatory_sunset(observatory, filter_sched, twilight=-18) + assert observatory.observatory.current_filter == "g" + assert observatory.conditions.sun_alt < np.radians(18) + + def test_simple_greedy_survey(self): + # Just test that it still instantiates and provides observations. + observatory = get_ideal_model_observatory(dayobs=self.day_obs_start, survey_start=self.survey_start) + greedy = [simple_greedy_survey(filtername="r")] + scheduler = CoreScheduler(greedy) + observatory, scheduler, observations = sim_runner( + observatory, scheduler, filter_scheduler=None, survey_length=0.7 + ) + # Current survey_start_mjd should produce ~1000 visits + # but changing this to a short night could reduce the total number + self.assertTrue(len(observations) > 650) + self.assertTrue(observations["mjd"].max() - observations["mjd"].min() > 0.4) + self.assertTrue(np.unique(observations["scheduler_note"]).size == 1) + self.assertTrue(np.unique(observations["scheduler_note"])[0] == "simple greedy r") + # Check that we tracked things appropriately + self.assertTrue( + len(observations) == scheduler.survey_lists[0][0].extra_features["ObsRecorded"].feature + ) + self.assertTrue( + np.abs( + observations[-1]["mjd"] + - scheduler.survey_lists[0][0].extra_features["LastObs"].feature["mjd"] + ) + < 15 / 60 / 60 / 24 + ) + + def test_simple_pairs_survey(self): + # Just test that it still instantiates and provides observations. + observatory = get_ideal_model_observatory(dayobs=self.day_obs_start, survey_start=self.survey_start) + pairs = [simple_pairs_survey(filtername="r", filtername2="i")] + scheduler = CoreScheduler(pairs) + observatory, scheduler, observations = sim_runner( + observatory, scheduler, filter_scheduler=None, survey_length=0.7 + ) + # Current survey_start_mjd should produce over ~950 visits + # but changing this to a short night could reduce the total number + self.assertTrue(len(observations) > 650) + self.assertTrue(observations["mjd"].max() - observations["mjd"].min() > 0.4) + self.assertTrue(np.unique(observations["scheduler_note"]).size == 2) + # Check that we tracked things appropriately + self.assertTrue( + len(observations) == scheduler.survey_lists[0][0].extra_features["ObsRecorded"].feature + ) + self.assertTrue( + np.abs( + observations[-1]["mjd"] + - scheduler.survey_lists[0][0].extra_features["LastObs"].feature["mjd"] + ) + < 15 / 60 / 60 / 24 + ) + + def test_simple_field_survey(self): + # Just test that it still instantiates and provides observations. + observatory = get_ideal_model_observatory(dayobs=self.day_obs_start, survey_start=self.survey_start) + # This field ought to be observable at our current survey_start + ra = 150 + dec = 2.2 + field_name = "almost_cosmos" + field = [ + simple_field_survey( + field_ra_deg=ra, field_dec_deg=dec, field_name=field_name, science_program="BLOCK-TEST" + ) + ] + # Add a greedy survey backup because of the visit gap requirement in + # the default field survey + greedy = [simple_greedy_survey(filtername="r")] + scheduler = CoreScheduler([field, greedy]) + observatory, scheduler, observations = sim_runner( + observatory, scheduler, filter_scheduler=None, survey_length=0.7 + ) + # Current survey_start_mjd should produce over ~950 visits + # but changing this to a short night could reduce the total number + self.assertTrue(len(observations) > 650) + self.assertTrue(observations["mjd"].max() - observations["mjd"].min() > 0.4) + # Check some information about the observation notes and names + self.assertTrue(np.unique(observations["scheduler_note"]).size == 2) + self.assertTrue("almost_cosmos" in observations["scheduler_note"]) + self.assertTrue("almost_cosmos" in observations["target_name"]) + # Check that the field survey got lots of visits + field_obs = observations[np.where(observations["target_name"] == "almost_cosmos")] + self.assertTrue(field_obs.size > 200) + self.assertTrue(np.all(field_obs["science_program"] == "BLOCK-TEST")) + self.assertTrue(field[0].extra_features["ObsRecorded"].feature == field_obs.size) + self.assertTrue( + np.abs(field_obs[-1]["mjd"] - field[0].extra_features["LastObs"].feature["mjd"]) + < 15 / 60 / 60 / 24 + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/scheduler/test_surveys.py b/tests/scheduler/test_surveys.py index 23b5de17..01881df6 100644 --- a/tests/scheduler/test_surveys.py +++ b/tests/scheduler/test_surveys.py @@ -106,7 +106,7 @@ def test_field_survey_add_observations(self): # Try adding observations to survey one at a time. survey = surveys.FieldSurvey(bfs, RA=90.0, dec=-30.0, accept_obs=None) for obs, indx in zip(observations_list, indexes): - survey.add_observation(obs[0], indx=indx) + survey.add_observation(obs, indx=indx) self.assertTrue(survey.extra_features["ObsRecorded"].feature == len(observations_list)) self.assertTrue(survey.extra_features["LastObs"].feature["mjd"] == observations_list[-1]["mjd"]) # Try adding observations to survey in array @@ -119,7 +119,7 @@ def test_field_survey_add_observations(self): # Try adding observations to survey one at a time. survey = surveys.FieldSurvey(bfs, RA=90.0, dec=-30.0, accept_obs=["r band"]) for obs, indx in zip(observations_list, indexes): - survey.add_observation(obs[0], indx=indx) + survey.add_observation(obs, indx=indx) self.assertTrue(survey.extra_features["ObsRecorded"].feature == 5) self.assertTrue(survey.extra_features["LastObs"].feature["mjd"] == observations_list[4]["mjd"]) # Try adding observations to survey in array @@ -130,7 +130,7 @@ def test_field_survey_add_observations(self): # Try adding observations to survey one at a time. survey = surveys.FieldSurvey(bfs, RA=90.0, dec=-30.0, accept_obs=["r band", "g band"]) for obs, indx in zip(observations_list, indexes): - survey.add_observation(obs[0], indx=indx) + survey.add_observation(obs, indx=indx) self.assertTrue(survey.extra_features["ObsRecorded"].feature == 10) self.assertTrue(survey.extra_features["LastObs"].feature["mjd"] == observations_list[-1]["mjd"]) # Try adding observations to survey in array