diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py b/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py index 2b68de96458c95d1cd2e068f7916dc9851ff3a45..550efab2b7be2627304ce24c24f4cf95cd5cb9c0 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py +++ b/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py @@ -283,7 +283,9 @@ def can_run_anywhere_within_timewindow_with_sky_constraints(scheduling_unit: mod target_rise_and_set_times = coordinates_timestamps_and_stations_to_target_rise_and_set(angle1=angle1, angle2=angle2, direction_type=direction_type, timestamps=timestamps, stations=tuple(stations), angle_to_horizon=min_elevation) for station, times in target_rise_and_set_times.items(): for i in range(len(timestamps)): - if not (timestamps[i] > times[0]['rise'] and timestamps[i] < times[0]['set']): + if times[0]['always_above_horizon']: + continue + if times[0]['always_below_horizon'] or not (timestamps[i] > times[0]['rise'] and timestamps[i] < times[0]['set']): if task['specifications_template'] == 'calibrator observation': logger.info('min_calibrator_elevation=%s constraint is not met at timestamp=%s' % (min_elevation.rad, timestamps[i])) else: diff --git a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py index 98c1c41ea5d494bcff75f928fc2da1216777ff51..e8fadb2c6085117007f7913c8ecee0fa3808b434 100755 --- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py @@ -735,8 +735,11 @@ class TestSkyConstraints(unittest.TestCase): self.distance_mock.return_value = self.distance_data self.addCleanup(self.distance_patcher.stop) - self.target_rise_and_set_data = {"CS002": [{"rise": datetime(2020, 1, 1, 8, 0, 0), "set": datetime(2020, 1, 1, 12, 30, 0)}, - {"rise": datetime(2020, 1, 1, 8, 0, 0), "set": datetime(2020, 1, 1, 12, 30, 0)}]} + self.target_rise_and_set_data = {"CS002": [{"rise": datetime(2020, 1, 1, 8, 0, 0), "set": datetime(2020, 1, 1, 12, 30, 0), "always_above_horizon": False, "always_below_horizon": False}, + {"rise": datetime(2020, 1, 1, 8, 0, 0), "set": datetime(2020, 1, 1, 12, 30, 0), "always_above_horizon": False, "always_below_horizon": False}]} + self.target_rise_and_set_data_always_above = {"CS002": [{"rise": None, "set": None, "always_above_horizon": True, "always_below_horizon": False}]} + self.target_rise_and_set_data_always_below = {"CS002": [{"rise": None, "set": None, "always_above_horizon": False, "always_below_horizon": True}]} + self.target_rise_and_set_patcher = mock.patch('lofar.sas.tmss.services.scheduling.constraints.template_constraints_v1.coordinates_timestamps_and_stations_to_target_rise_and_set') self.target_rise_and_set_mock = self.target_rise_and_set_patcher.start() self.target_rise_and_set_mock.return_value = self.target_rise_and_set_data @@ -760,21 +763,45 @@ class TestSkyConstraints(unittest.TestCase): # min_target_elevation - def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_constraint_returns_true_when_met(self): + def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_constraint_returns_true(self): + self.target_rise_and_set_mock.return_value = self.target_rise_and_set_data + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_target_elevation': 0.1} self.scheduling_unit_blueprint.save() - timestamp = datetime(2020, 1, 1, 10, 0, 0) + timestamp = datetime(2020, 1, 1, 10, 0, 0) # target sets after obs ends (mocked response) returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration)) self.assertTrue(returned_value) - def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_constraint_returns_false_when_not_met(self): - self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_target_elevation': 0.2} + def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_when_target_always_above_returns_true(self): + self.target_rise_and_set_mock.return_value = self.target_rise_and_set_data_always_above + + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_target_elevation': 0.1} self.scheduling_unit_blueprint.save() - timestamp = datetime(2020, 1, 1, 11, 0, 0) + timestamp = datetime(2020, 1, 1, 10, 0, 0) # target is always up (mocked response) + returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration)) + self.assertTrue(returned_value) + + def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_constraint_returns_false(self): + self.target_rise_and_set_mock.return_value = self.target_rise_and_set_data + + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_target_elevation': 0.1} + self.scheduling_unit_blueprint.save() + timestamp = datetime(2020, 1, 1, 11, 0, 0) # target sets before obs ends (mocked response) + returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration)) + self.assertFalse(returned_value) + + def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_when_target_is_always_below_returns_false(self): + self.target_rise_and_set_mock.return_value = self.target_rise_and_set_data_always_below + + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_target_elevation': 0.1} + self.scheduling_unit_blueprint.save() + timestamp = datetime(2020, 1, 1, 10, 0, 0) # target is never up (mocked response) returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration)) self.assertFalse(returned_value) + + class TestTimeConstraints(TestCase): """ Tests for the time constraint checkers used in dynamic scheduling with different boundaries diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py b/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py index ae926e172f4a39a4ff77a442346fbf25d4505e35..3c0e184ce79ac8e697043dcf8ced5dceba3bf1eb 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py @@ -126,9 +126,10 @@ def coordinates_timestamps_and_stations_to_target_rise_and_set(angle1: float, an :param stations: tuple of station names, e.g. ("CS002",) :param angle_to_horizon: the angle between horizon and given coordinates for which rise and set times are returned :return A dict that maps station names to a list of dicts with rise and set times for each requested date. + If rise and set are None, the target is always above or below horizon, and the respective boolean is True. E.g. - {"CS002": [{"rise": datetime(2020, 1, 1, 4, 0, 0), "set": datetime(2020, 1, 1, 11, 0, 0)}, - {"rise": datetime(2020, 1, 2, 4, 0, 0), "set": datetime(2020, 1, 2, 11, 0, 0)}] + {"CS002": [{"rise": datetime(2020, 1, 1, 4, 0, 0), "set": datetime(2020, 1, 1, 11, 0, 0), "always_above_horizon": False, "always_below_horizon": False}, + {"rise": datetime(2020, 1, 2, 4, 0, 0), "set": datetime(2020, 1, 2, 11, 0, 0), "always_above_horizon": False, "always_below_horizon": False}] } """ if direction_type == "J2000": @@ -140,10 +141,29 @@ def coordinates_timestamps_and_stations_to_target_rise_and_set(angle1: float, an for timestamp in timestamps: # todo: this can probably be made faster by moving the following logic to an own function with single station/timestamp as input and putting the lru_cache on there. observer = create_astroplan_observer_for_station(station) - target_set = observer.target_set_time(target=coord, time=Time(timestamp), horizon=angle_to_horizon, which='next', n_grid_points=TARGET_SET_RISE_PRECISION) - target_rise = observer.target_rise_time(target=coord, time=Time(target_set), horizon=angle_to_horizon, which='previous', n_grid_points=TARGET_SET_RISE_PRECISION) + try: + target_set = observer.target_set_time(target=coord, time=Time(timestamp), horizon=angle_to_horizon, which='next', n_grid_points=TARGET_SET_RISE_PRECISION) + target_rise = observer.target_rise_time(target=coord, time=Time(target_set), horizon=angle_to_horizon, which='previous', n_grid_points=TARGET_SET_RISE_PRECISION) + return_dict.setdefault(station, []).append( + {"rise": target_rise.to_datetime(), + "set": target_set.to_datetime(), + "always_above_horizon": False, + "always_below_horizon": False}) + except TypeError as e: + if "numpy.float64" in str(e): + # Note: when the target is always above or below horizon, astroplan excepts with the not very + # meaningful error: 'numpy.float64' object does not support item assignment + # Determine whether the target is always above or below horizon so that we can return some useful + # additional info, e.g. for scheduling purposes. + is_up = observer.target_is_up(target=coord, time=Time(timestamp), horizon=angle_to_horizon) + return_dict.setdefault(station, []).append( + {"rise": None, + "set": None, + "always_above_horizon": is_up, + "always_below_horizon": not is_up}) + else: + raise - return_dict.setdefault(station, []).append({"rise": target_rise.to_datetime(), "set": target_set.to_datetime()}) return return_dict diff --git a/SAS/TMSS/backend/test/t_conversions.py b/SAS/TMSS/backend/test/t_conversions.py index 7f8d66d6e4b8758b3cf13bf04bf3d8488deb89ad..1773168c7b1ded14c41aee27f0fddd6683d9f9f7 100755 --- a/SAS/TMSS/backend/test/t_conversions.py +++ b/SAS/TMSS/backend/test/t_conversions.py @@ -362,6 +362,35 @@ class UtilREST(unittest.TestCase): self.assertNotEqual(rise, rise_last) rise_last = rise + def test_util_target_rise_and_set_detects_when_target_above_horizon(self): + + # assert always below and always above are usually false + r = requests.get(BASE_URL + '/util/target_rise_and_set?angle1=0.5&angle2=0.8×tamps=2020-01-01&horizon=0.2', auth=AUTH) + self.assertEqual(r.status_code, 200) + r_dict = json.loads(r.content.decode('utf-8')) + self.assertIsNotNone(r_dict['CS002'][0]['rise']) + self.assertIsNotNone(r_dict['CS002'][0]['set']) + self.assertFalse(r_dict['CS002'][0]['always_below_horizon']) + self.assertFalse(r_dict['CS002'][0]['always_above_horizon']) + + # assert rise and set are None and flag is true when target is always above horizon + r = requests.get(BASE_URL + '/util/target_rise_and_set?angle1=0.5&angle2=0.8×tamps=2020-01-01&horizon=0.1', auth=AUTH) + self.assertEqual(r.status_code, 200) + r_dict = json.loads(r.content.decode('utf-8')) + self.assertIsNone(r_dict['CS002'][0]['rise']) + self.assertIsNone(r_dict['CS002'][0]['set']) + self.assertTrue(r_dict['CS002'][0]['always_above_horizon']) + self.assertFalse(r_dict['CS002'][0]['always_below_horizon']) + + # assert rise and set are None and flag is true when target is always below horizon + r = requests.get(BASE_URL + '/util/target_rise_and_set?angle1=0.5&angle2=-0.5×tamps=2020-01-01&horizon=0.2', auth=AUTH) + self.assertEqual(r.status_code, 200) + r_dict = json.loads(r.content.decode('utf-8')) + self.assertIsNone(r_dict['CS002'][0]['rise']) + self.assertIsNone(r_dict['CS002'][0]['set']) + self.assertFalse(r_dict['CS002'][0]['always_above_horizon']) + self.assertTrue(r_dict['CS002'][0]['always_below_horizon']) + if __name__ == "__main__": os.environ['TZ'] = 'UTC'