diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints.py b/SAS/TMSS/backend/services/scheduling/lib/constraints.py index cd9f4f031b42b7427c1f6c0688f10a1f3a806473..44f7225921f69e7384c92e9d5e7dab8320fd4f60 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/constraints.py +++ b/SAS/TMSS/backend/services/scheduling/lib/constraints.py @@ -773,7 +773,7 @@ def evaluate_sky_transit_constraint(scheduling_unit: models.SchedulingUnitBluepr # now check if the constraint is met, and compute/set score # include the margins for gridding effects when checking offset-within-window, # but not when computing score - offset = (task_proposed_center_time-transit_timestamp).total_seconds() + offset = int((task_proposed_center_time-transit_timestamp).total_seconds()) if offset > transit_from_limit_with_margin and offset < transit_to_limit_with_margin: # constraint is met. compute score. # 1.0 when proposed_center_time==transit_timestamp @@ -786,6 +786,11 @@ def evaluate_sky_transit_constraint(scheduling_unit: models.SchedulingUnitBluepr result.score = min(1.0, max(0.0, score)) else: result.score = 0 + result.message = "offset of %s[s] at task_center='%s' from transit at '%s' at %s for %s is not within [%s, %s]" % (offset, task_proposed_center_time, transit_timestamp, station, pointing, transit_from_limit, transit_to_limit) + + # log and early exit, cause the constraint is not met. + logger.debug(result) + return result return result @@ -868,9 +873,11 @@ def evaluate_sky_min_elevation_constraint(scheduling_unit: models.SchedulingUnit (rise_and_set_time['set'] is not None and timestamp > gridder.minus_margin(rise_and_set_time['set'])): # constraint not met. update result, and do early exit. result.score = 0 - result.evaluation_timestamp = timestamp + result.evaluation_timestamp = gridded_timestamp result.optimal_start_time = None - logger.debug("%s task_id=%s task_name='%s' station=%s min_elevation=%.3f[deg] rise='%s' set='%s'", result, task.id, task.name, station, min_elevation.degree, rise_and_set_time['rise'], rise_and_set_time['set']) + elevation = compute_elevation(pointing, gridded_timestamp, station) + result.message = "task_id=%s task_name='%s' station=%s elevation=%.3f[deg] < min_elevation=%.3f[deg] at '%s'" % (task.id, task.name, station, Angle(elevation, astropy.units.rad).deg, min_elevation.degree, gridded_timestamp) + logger.debug(result) return result # for min_elevation there is no optimal start time. @@ -1692,10 +1699,14 @@ def determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_u if 'min_elevation' in scheduling_unit.scheduling_constraints_doc['sky']: if get_earliest_possible_start_time_for_sky_min_elevation(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped) is None: unmet_constraints.append("sky min_elevation") + result = evaluate_sky_min_elevation_constraint(scheduling_unit, lower_bound, gridder=gridder) + reasons.append(result.message) if 'transit_offset' in scheduling_unit.scheduling_constraints_doc['sky']: if get_earliest_possible_start_time_for_sky_transit_offset(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped) is None: unmet_constraints.append("sky transit_offset") + result = evaluate_sky_transit_constraint(scheduling_unit, lower_bound, gridder=gridder) + reasons.append(result.message) if 'min_distance' in scheduling_unit.scheduling_constraints_doc['sky']: if get_earliest_possible_start_time_for_sky_min_distance(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped) is None: @@ -1710,10 +1721,13 @@ def determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_u if 'daily' in scheduling_unit.scheduling_constraints_doc: if get_earliest_possible_start_time_for_daily_constraints(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped) is None: unmet_constraints.append("daily") + result = evaluate_daily_constraints(scheduling_unit, lower_bound, gridder=gridder) + reasons.append(result.message) if unmet_constraints: at = get_at_constraint_timestamp(scheduling_unit) msg = ', '.join(unmet_constraints) + (" constraint is" if len(unmet_constraints)==1 else " constraints are") + " not met " + ("at %s" % (round_to_second_precision(at),) if at else "anywhere between %s and %s" % (round_to_second_precision(lower_bound), round_to_second_precision(upper_bound))) + reasons = [r for r in reasons if isinstance(r, str) and len(r)] if reasons: msg += '\nreason(s): ' + '\n'.join(reasons) mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(scheduling_unit, msg) 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 8e456b46728ecfefe1520c9b011195fa1d2ad9db..5b56b8eaa9dcd47440b1b7a67fff3638edb81435 100755 --- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py @@ -1577,16 +1577,16 @@ class TestDynamicScheduling(BaseDynamicSchedulingTestCase): weight_factor.weight = 0 weight_factor.save() - for expected_reason, constraints in (("constraint time.at='2029-12-31 00:00:00' falls outside of window ['2030-01-01 00:00:00', '2030-01-08 00:00:00']", {'time': {'at': (cycle.start - timedelta(hours=24)).isoformat()}}), - ("time constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00", {'time': {'before': (cycle.start - timedelta(hours=24)).isoformat()}}), - ("time constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00", {'time': {'after': (cycle.stop + timedelta(hours=24)).isoformat()}}), - ("sky min_elevation constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00", {'sky': {'min_elevation': {'target': 1.57}}}), - ("sky min_distance constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00", {'sky': {'min_distance': {'sun': 1.57}}}), - ("sky transit_offset constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-01 07:00:00", {'sky': {'transit_offset': {'from': -900, 'to': 900}}, 'time': {'between': [{'from': '2030-01-01T00:00:00Z', 'to': '2030-01-01T07:00:00Z'}]}}), - ("daily constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-01 07:00:00", {'daily': {'require_day': True }, 'time': {'between': [{'from': '2030-01-01T00:00:00Z', 'to': '2030-01-01T07:00:00Z'}]}}), - ("daily constraint is not met anywhere between 2030-01-01 11:00:00 and 2030-01-01 15:00:00", {'daily': {'require_night': True }, 'time': {'between': [{'from': '2030-01-01T11:00:00Z', 'to': '2030-01-01T15:00:00Z'}]}}), - ("daily constraint is not met anywhere between 2030-01-01 07:00:00 and 2030-01-01 09:00:00", {'daily': {'avoid_twilight': True }, 'time': {'between': [{'from': '2030-01-01T07:00:00Z', 'to': '2030-01-01T09:00:00Z'}]}}), - ): + for expected_reason_start, expected_specific_reason_part, constraints in ( + ("constraint time.at='2029-12-31 00:00:00' falls outside of window ['2030-01-01 00:00:00', '2030-01-08 00:00:00']", '', {'time': {'at': (cycle.start - timedelta(hours=24)).isoformat()}}), + ("time constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00", '', {'time': {'before': (cycle.start - timedelta(hours=24)).isoformat()}}), + ("time constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00", '', {'time': {'after': (cycle.stop + timedelta(hours=24)).isoformat()}}), + ("sky min_elevation constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00", "task_name='Observation' station=CS002 elevation=", {'sky': {'min_elevation': {'target': 1.57}}}), + ("sky min_distance constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00", 'to body=sun < min_distance=149.9', {'sky': {'min_distance': {'sun': 150 * 3.1415/180.0}}}), + ("sky transit_offset constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-01 07:00:00", '', {'sky': {'transit_offset': {'from': -900, 'to': 900}}, 'time': {'between': [{'from': '2030-01-01T00:00:00Z', 'to': '2030-01-01T07:00:00Z'}]}}), + ("daily constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-01 07:00:00", '', {'daily': {'require_day': True }, 'time': {'between': [{'from': '2030-01-01T00:00:00Z', 'to': '2030-01-01T07:00:00Z'}]}}), + ("daily constraint is not met anywhere between 2030-01-01 11:00:00 and 2030-01-01 15:00:00", '', {'daily': {'require_night': True }, 'time': {'between': [{'from': '2030-01-01T11:00:00Z', 'to': '2030-01-01T15:00:00Z'}]}}), + ("daily constraint is not met anywhere between 2030-01-01 07:00:00 and 2030-01-01 09:00:00", '', {'daily': {'avoid_twilight': True }, 'time': {'between': [{'from': '2030-01-01T07:00:00Z', 'to': '2030-01-01T09:00:00Z'}]}}) ): # reset unit... mark_independent_subtasks_in_scheduling_unit_blueprint_as_schedulable(scheduling_unit_blueprint) self.assertEqual('', scheduling_unit_blueprint.unschedulable_reason) @@ -1603,7 +1603,8 @@ class TestDynamicScheduling(BaseDynamicSchedulingTestCase): # Assert the scheduling_unit has not been scheduled and that it has the correct expected unschedulable_reason scheduling_unit_blueprint.refresh_from_db() self.assertEqual('unschedulable', scheduling_unit_blueprint.status.value) - self.assertEqual(expected_reason, scheduling_unit_blueprint.unschedulable_reason) + self.assertTrue(scheduling_unit_blueprint.unschedulable_reason.startswith(expected_reason_start)) + self.assertTrue(expected_specific_reason_part in scheduling_unit_blueprint.unschedulable_reason) def test_clear_unschedulable_reason_TMSS_1881_bugfix(self): '''Test if the unschedulable reason is cleared for cleanup task. @@ -1666,6 +1667,8 @@ class TestDynamicScheduling(BaseDynamicSchedulingTestCase): This test uses a specification like https://tmss.lofar.eu/api/scheduling_unit_blueprint/1645/specifications_doc But with a min_distance constraint that is tuned to fail, such that we can check the usefulness of the unschedulable_reason. + + For all other constraints and reasons see test_unschedulable_reasons_due_to_unmet_constraints. ''' project = models.Project.objects.create(**Project_test_data(name=str(uuid.uuid4()), project_state=models.ProjectState.objects.get(value=models.ProjectState.Choices.ACTIVE.value))) scheduling_set = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=project))