From cbb46ea9f1c210a3494bb5686eaeea96c643aa6e Mon Sep 17 00:00:00 2001 From: Jorrit Schaap <schaap@astron.nl> Date: Tue, 7 Mar 2023 09:48:04 +0100 Subject: [PATCH] TMSS-2295: implemented fix for TMSS-2295. + minor fixes --- .../services/scheduling/lib/constraints.py | 87 +++++++++++-------- .../scheduling/lib/dynamic_scheduling.py | 22 +---- .../scheduling/test/t_dynamic_scheduling.py | 28 +++--- 3 files changed, 70 insertions(+), 67 deletions(-) diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints.py b/SAS/TMSS/backend/services/scheduling/lib/constraints.py index 6ba03a8b699..249dca148bc 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/constraints.py +++ b/SAS/TMSS/backend/services/scheduling/lib/constraints.py @@ -28,7 +28,7 @@ These main methods are used in the dynamic_scheduler to pick the next best sched from datetime import datetime, timedelta from dateutil import parser -from typing import Callable, Union +from typing import Callable, Union, Tuple, Iterable from astropy.coordinates import Angle from astropy.coordinates.earth import EarthLocation import astropy.units @@ -192,7 +192,7 @@ class ConstraintResult(): @lru_cache(100, typed=False) -def get_boundary_stations_from_list(stations: [str]) -> (str, str, str, str): +def get_boundary_stations_from_list(stations: Tuple[str]) -> Tuple[str]: ''' utility function to determine the stations at boundary locations. Meant to be used for constraint checks, since when constraints are met at these stations, this is typically also true at the other stations. @@ -219,6 +219,11 @@ def get_boundary_stations_from_list(stations: [str]) -> (str, str, str, str): return most_northern, most_eastern, most_southern, most_western +def get_unique_sorted_boundary_stations_or_cs002(stations: Iterable[str]) -> Tuple[str]: + boundary_stations = set(get_boundary_stations_from_list(tuple(stations))) or {'CS002'} + return tuple(sorted(list(boundary_stations))) + + def filter_scheduling_units_using_constraints(scheduling_units: [models.SchedulingUnitBlueprint], lower_bound: datetime, upper_bound: datetime, raise_if_interruped: Callable=noop, gridder: Gridder=None) -> [models.SchedulingUnitBlueprint]: """ Filter the given scheduling_units by whether their constraints are met within the given timewindow. @@ -243,7 +248,7 @@ def filter_scheduling_units_using_constraints(scheduling_units: [models.Scheduli assert(scheduling_unit.draft is not None) assert(scheduling_unit.scheduling_constraints_template is not None) - if can_run_within_station_reservations(scheduling_unit): + if can_run_within_station_reservations(scheduling_unit): # ToDo: submit proposed start time, do not rely on set scheduled_start/stop_time(s) if can_run_within_timewindow_with_constraints(scheduling_unit, lower_bound, upper_bound, gridder): if can_run_within_cycles_bounds(scheduling_unit, lower_bound, upper_bound): runnable_scheduling_units.append(scheduling_unit) @@ -296,10 +301,8 @@ def filter_scheduling_units_which_can_only_run_in_this_window(scheduling_units: earliest_possible_start_time = min(earliest_possible_start_time, upper_bound) if not can_run_after(scheduling_unit, earliest_possible_start_time, gridder=gridder): - if not can_run_before(scheduling_unit, lower_bound, gridder=gridder): - runnable_exclusive_in_this_window_scheduling_units.append(scheduling_unit) - if lower_bound < datetime.utcnow() + timedelta(minutes=1): - # no unit can run before 'now' + if (not can_run_before(scheduling_unit, lower_bound, gridder=gridder) or + (lower_bound < datetime.utcnow() + timedelta(minutes=1)) ): # no unit can run before 'now' runnable_exclusive_in_this_window_scheduling_units.append(scheduling_unit) logger.info("filter_scheduling_units_which_can_only_run_in_this_window: checked unit [%d/%d] %.1f%% id=%d can %srun outside of window ['%s', '%s']", @@ -316,7 +319,7 @@ def filter_scheduling_units_which_can_only_run_in_this_window(scheduling_units: return runnable_exclusive_in_this_window_scheduling_units -def get_best_scored_scheduling_unit_scored_by_constraints(scheduling_units: [models.SchedulingUnitBlueprint], lower_bound_start_time:datetime, upper_bound_stop_time:datetime, coarse_gridder: Gridder, fine_gridder: Gridder) -> ScoredSchedulingUnit: +def get_best_scored_scheduling_unit_scored_by_constraints(scheduling_units: [models.SchedulingUnitBlueprint], lower_bound_start_time:datetime, upper_bound_stop_time:datetime, coarse_gridder: Gridder, fine_gridder: Gridder, raise_if_interruped: Callable=noop) -> ScoredSchedulingUnit: """ get the best scored schedulable scheduling_unit which can run withing the given time window from the given scheduling_units. :param lower_bound_start_time: evaluate and score the constrains at and after lower_bound_start_time. The returned unit has a start_time guaranteed at or after lower_bound_start_time. @@ -338,9 +341,14 @@ def get_best_scored_scheduling_unit_scored_by_constraints(scheduling_units: [mod # Re-evaluate top 5(or less) with fine grid top_sorted_scored_scheduling_units_coarse = sorted_scored_scheduling_units_coarse[:5] - logger.info("get_best_scored_scheduling_unit_scored_by_constraints: scoring and sorting %d units at fine grid of %s[min]...", len(top_sorted_scored_scheduling_units_coarse), fine_gridder.grid_minutes) - top_scheduling_units = [x.scheduling_unit for x in top_sorted_scored_scheduling_units_coarse] - top_sorted_scored_scheduling_units_fine = sort_scheduling_units_scored_by_constraints(top_scheduling_units, lower_bound_start_time, upper_bound_stop_time, fine_gridder) + logger.info("get_best_scored_scheduling_unit_scored_by_constraints: (re)filtering, (re)scoring and sorting %d units at fine grid of %s[min]...", len(top_sorted_scored_scheduling_units_coarse), fine_gridder.grid_minutes) + top_scheduling_units_coarse = [x.scheduling_unit for x in top_sorted_scored_scheduling_units_coarse] + + # First check if the top5 can also run when evaluated at the fine grid. There may be edge cases. + top_scheduling_units_fine = filter_scheduling_units_using_constraints(top_scheduling_units_coarse, lower_bound_start_time, upper_bound_stop_time, gridder=fine_gridder, raise_if_interruped=raise_if_interruped) + + # compute te scores at the fine grid + top_sorted_scored_scheduling_units_fine = sort_scheduling_units_scored_by_constraints(top_scheduling_units_fine, lower_bound_start_time, upper_bound_stop_time, fine_gridder) _method_elapsed = datetime.utcnow() - _method_start_timestamp logger.debug("get_best_scored_scheduling_unit_scored_by_constraints: scored and sorted %d units (took %.1f[s])", len(scheduling_units), _method_elapsed.total_seconds()) @@ -775,8 +783,7 @@ def evaluate_sky_transit_constraint(scheduling_unit: models.SchedulingUnitBluepr task_proposed_start_time = proposed_start_time + target_obs_task.relative_start_time task_proposed_center_time = task_proposed_start_time + (target_obs_task.specified_duration / 2) - # TODO: determine transit opt/earliest times over all boundery stations - stations = ('CS002',) #set(get_boundary_stations_from_list(target_obs_task.specified_stations)) or ('CS002',) + stations = get_unique_sorted_boundary_stations_or_cs002(target_obs_task.specified_stations) # currently we only check at bounds and center, we probably want to add some more samples in between later on for pointing in transit_pointings: @@ -840,7 +847,7 @@ def evaluate_sky_min_elevation_constraint(scheduling_unit: models.SchedulingUnit return result # limit stations to be evaluated to the edge stations - stations = set(get_boundary_stations_from_list(tuple(task.specified_stations))) or ('CS002',) + stations = get_unique_sorted_boundary_stations_or_cs002(tuple(task.specified_stations)) # determine the min_elevation and stations depending on observation type pointings_and_min_elevations = [] @@ -869,14 +876,17 @@ def evaluate_sky_min_elevation_constraint(scheduling_unit: models.SchedulingUnit # currently we only check at bounds and center, we probably want to add some more samples in between later on for timestamp in (task_proposed_start_time, task_proposed_center_time, task_proposed_end_time): - for pointing, min_elevation in pointings_and_min_elevations: - gridded_timestamp = gridder.grid_time(timestamp) - station_target_rise_and_set_times = coordinates_timestamps_and_stations_to_target_rise_and_set(pointing=pointing, - timestamps=(gridded_timestamp,), - stations=tuple(stations), - angle_to_horizon=min_elevation, - n_grid_points=gridder.n_grid_points()) - for station, target_rise_and_set_times in station_target_rise_and_set_times.items(): + for station in stations: + for pointing, min_elevation in pointings_and_min_elevations: + gridded_timestamp = gridder.grid_time(timestamp) + station_target_rise_and_set_times = coordinates_timestamps_and_stations_to_target_rise_and_set(pointing=pointing, + timestamps=(gridded_timestamp,), + stations=(station,), + angle_to_horizon=min_elevation, + n_grid_points=gridder.n_grid_points()) + assert len(station_target_rise_and_set_times) == 1 + assert station in station_target_rise_and_set_times + target_rise_and_set_times = station_target_rise_and_set_times[station] assert len(target_rise_and_set_times) == 1 rise_and_set_time = target_rise_and_set_times[0] if rise_and_set_time['rise'] is not None: @@ -896,16 +906,17 @@ def evaluate_sky_min_elevation_constraint(scheduling_unit: models.SchedulingUnit # check if constraint is met... if rise_and_set_time['always_below_horizon'] or \ - (rise_and_set_time['rise'] is not None and timestamp < gridder.plus_margin(rise_and_set_time['rise'])) or \ - (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 = gridded_timestamp - result.optimal_start_time = None + (rise_and_set_time['rise'] is not None and timestamp < gridder.minus_margin(rise_and_set_time['rise'])) or \ + (rise_and_set_time['set'] is not None and timestamp > gridder.plus_margin(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 + if elevation < min_elevation.rad: + # constraint not met. update result, and do early exit. + result.score = 0 + result.evaluation_timestamp = gridded_timestamp + result.optimal_start_time = None + result.message = "task_id=%s task_name='%s' station=%s target='%s' elevation=%.3f[deg] < min_elevation=%.3f[deg] at '%s'" % (task.id, task.name, station, pointing.target, Angle(elevation, astropy.units.rad).deg, min_elevation.degree, gridded_timestamp) + logger.info(result) + return result # for min_elevation there is no optimal start time. # any timestamp meeting the constraint is good enough. @@ -1044,12 +1055,12 @@ def get_earliest_possible_start_time_for_sky_min_elevation(scheduling_unit: mode # do expensive search from lower_bound until 24 hours later with small steps # (sky constrains are (almost) cyclic over 24 hours). # first occurrence where min_elevation constraint is met is taken as rough estimate of earliest_possible_start_time - gridded_lower_bound = gridder.grid_time(lower_bound) - possible_start_time = gridded_lower_bound + at = get_at_constraint_timestamp(scheduling_unit) + possible_start_time = at or gridder.grid_time(lower_bound) if upper_bound is None: - upper_bound = lower_bound + timedelta(hours=24) + upper_bound = lower_bound + (scheduling_unit.specified_observation_duration if at else timedelta(hours=24)) upper_bound = max(lower_bound, upper_bound) - while possible_start_time < upper_bound: + while possible_start_time <= upper_bound-scheduling_unit.specified_observation_duration: raise_if_interruped() result = evaluate_sky_min_elevation_constraint(scheduling_unit, possible_start_time, gridder=gridder) @@ -1188,7 +1199,7 @@ def evaluate_daily_constraints(scheduling_unit: models.SchedulingUnitBlueprint, duration = scheduling_unit.specified_observation_duration proposed_end_time = proposed_start_time + duration - stations = set(get_boundary_stations_from_list(tuple(scheduling_unit.main_observation_stations))) or ('CS002',) + stations = get_unique_sorted_boundary_stations_or_cs002(tuple(scheduling_unit.main_observation_stations)) # the sun rise/set events do not depend on the actual time-of-day, but only on the date. # so, use one timstamp for 'today'-noon, one for 'yesterday'-noon and one for 'tomorrow'-noon @@ -1390,7 +1401,7 @@ def get_earliest_possible_start_time_for_time_constraints(scheduling_unit: model earliest_possible_start_times.add(potential_earliest_possible) break - if not earliest_possible_start_times and not constraints.get('time'): + if not earliest_possible_start_times and not (constraints.get('time') or constraints.get('time',{}).get('between') or constraints.get('time',{}).get('not_between')): # an empty time constraint means it can just run at/after lower_bound return lower_bound @@ -1424,7 +1435,7 @@ def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBluep if at: # if there is an 'at' constraint, then that is always earliest_possible_start_time # however... we should also check if all other constraints are met. So, can it run at 'at'? - if can_run_at(scheduling_unit, at): + if can_run_at(scheduling_unit, at, gridder=gridder): return at return None diff --git a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py index 97d463d7ea4..f8cb43c17b3 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py @@ -97,8 +97,8 @@ class Scheduler: self._scheduling_thread = None self._scheduling_thread_running = False self._do_schedule_event = Event() - self.search_gridder = Gridder(grid_minutes=Gridder.VERY_COARSE_TIME_GRID) - self.fine_gridder = Gridder(grid_minutes=Gridder.DEFAULT_TIME_GRID) + self.search_gridder = Gridder(grid_minutes=1*60) + self.fine_gridder = Gridder(grid_minutes=1) super().__init__() # make sure initial status is idle models.Subsystem.Activator('scheduler').deactivate() @@ -359,14 +359,8 @@ class Scheduler: logger.info("find_best_next_schedulable_unit: units to score in window ['%s', '%s']: %s", lower_bound_start_time, upper_bound_stop_time, ','.join([str(su.id) for su in sorted(units_to_score, key=lambda x: x.id)]) or 'None') - #ToDo: TMSS-1745 Add unschedulable reasons - # implementation hint: subtract units_to_score from scheduling_units giving you the not_to_be_scheduled_units list - # then ask for each unit in this list if it can be run after lower_bound_start_time - # if not, call each evaluate_x_contraints method. If contraint not met, then you know which constraint is not met and why. - # return the correct reason as given in the ticket for each un-met constraint type. - # from the filtered down list of units, compute the (weighted) scores, and return the best scoring one. - best_scored_scheduling_unit = get_best_scored_scheduling_unit_scored_by_constraints(units_to_score, lower_bound_start_time, upper_bound_stop_time, self.search_gridder, self.fine_gridder) + best_scored_scheduling_unit = get_best_scored_scheduling_unit_scored_by_constraints(units_to_score, lower_bound_start_time, upper_bound_stop_time, self.search_gridder, self.fine_gridder, raise_if_interruped=self._raise_if_triggered) _elapsed = datetime.utcnow() - _start_search_timestamp @@ -383,10 +377,6 @@ class Scheduler: Overlapping existing scheduled units are unscheduled if their score is lower. :return: the scheduled scheduling unit.''' - # use relatively fine gridders for scheduling the upcoming unit. - self.search_gridder = Gridder(grid_minutes=1*60) - self.fine_gridder = Gridder(grid_minutes=1) - # prepare queries for subsets of schedulable_units (django uses lazy evaluation, so don't worry about wasted queries) schedulable_units_triggered = get_triggered_schedulable_scheduling_units() schedulable_units_queue_A = get_dynamically_schedulable_scheduling_units(priority_queue=models.PriorityQueueType.objects.get(value=models.PriorityQueueType.Choices.A.value)) @@ -579,10 +569,6 @@ class Scheduler: scheduling_unit.placed = False scheduling_unit.save() - # use relatively coarser gridders for mid-term schedule. Just to get a rough idea. - self.search_gridder = Gridder(grid_minutes=3*60) - self.fine_gridder = Gridder(grid_minutes=15) - priority_queue_A = models.PriorityQueueType.objects.get(value=models.PriorityQueueType.Choices.A.value) priority_queue_B = models.PriorityQueueType.objects.get(value=models.PriorityQueueType.Choices.B.value) @@ -700,7 +686,7 @@ def mark_scheduling_units_for_inactive_projects_unschedulable(projects: [str]=No logger.info("marking unit id=%s status= %s project=%s pstate=%s", scheduling_unit.id, scheduling_unit.status, scheduling_unit.project.name, scheduling_unit.project.project_state.value) with transaction.atomic(): mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(scheduling_unit, reason="project %s is not active"%(scheduling_unit.project.name,)) - set_scheduling_unit_blueprint_start_times(scheduling_unit, first_start_time=round_to_second_precision(datetime.utcnow())) + set_scheduling_unit_blueprint_start_times(scheduling_unit, first_start_time=round_to_second_precision(get_at_constraint_timestamp(scheduling_unit) or datetime.utcnow())) def mark_unschedulable_scheduling_units_for_active_projects_schedulable(projects: [str]=None): 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 8b4585ec7f1..b97075f377c 100755 --- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py @@ -1581,8 +1581,8 @@ class TestDynamicScheduling(BaseDynamicSchedulingTestCase): ("constraint time.at='2029-12-31 00:00:00' falls outside of cycle bounds ['2030-01-01 00:00:00', '2030-01-07 00:00:00']", '', {'scheduler': 'dynamic', '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", '', {'scheduler': 'dynamic', '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", '', {'scheduler': 'dynamic', '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=", {'scheduler': 'dynamic', 'sky': {'min_elevation': {'target': 1.57}}}), - ("sky min_elevation constraint is not met at 2030-01-01 00:00:00", "task_name='Observation' station=CS002 elevation=", {'scheduler': 'fixed_time', 'sky': {'min_elevation': {'target': 1.57}}, 'time': {'at': '2030-01-01T00:00:00Z'}}), + ("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 target='_target_name_' elevation=", {'scheduler': 'dynamic', 'sky': {'min_elevation': {'target': 1.57}}}), + ("sky min_elevation constraint is not met at 2030-01-01 00:00:00", "task_name='Observation' station=CS002 target='_target_name_' elevation=", {'scheduler': 'fixed_time', 'sky': {'min_elevation': {'target': 1.57}}, 'time': {'at': '2030-01-01T00:00:00Z'}}), ("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', {'scheduler': 'dynamic', 'sky': {'min_distance': {'sun': 150 * 3.1415/180.0}}}), ("sky min_distance constraint is not met at 2030-01-01 00:00:00", 'to body=sun < min_distance=149.9', {'scheduler': 'fixed_time', 'sky': {'min_distance': {'sun': 150 * 3.1415/180.0}}, 'time': {'at': '2030-01-01T00:00:00Z'}}), ("sky transit_offset constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-01 07:00:00", '', {'scheduler': 'dynamic', 'sky': {'transit_offset': {'from': -900, 'to': 900}}, 'time': {'between': [{'from': '2030-01-01T00:00:00Z', 'to': '2030-01-01T07:00:00Z'}]}}), @@ -1609,8 +1609,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.assertTrue(scheduling_unit_blueprint.unschedulable_reason.startswith(expected_reason_start)) - self.assertTrue(expected_specific_reason_part in scheduling_unit_blueprint.unschedulable_reason) + self.assertTrue(scheduling_unit_blueprint.unschedulable_reason.startswith(expected_reason_start), msg="expected: %s\nactual:%s"%(expected_reason_start, scheduling_unit_blueprint.unschedulable_reason)) + self.assertTrue(expected_specific_reason_part in scheduling_unit_blueprint.unschedulable_reason, msg="expected: %s\nactual:%s"%(expected_specific_reason_part, scheduling_unit_blueprint.unschedulable_reason)) def test_clear_unschedulable_reason_TMSS_1881_bugfix(self): '''Test if the unschedulable reason is cleared for cleanup task. @@ -1621,20 +1621,26 @@ class TestDynamicScheduling(BaseDynamicSchedulingTestCase): scheduling_set = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=project)) strategy_template = models.SchedulingUnitObservingStrategyTemplate.get_latest(name="IM HBA - 1 Beam") su_draft = create_scheduling_unit_draft_from_observing_strategy_template(strategy_template, scheduling_set=scheduling_set) + su_draft.scheduling_constraints_doc['scheduler'] = 'dynamic' + # by default, allow very loose sky constraints which can basically run anywhere + su_draft.scheduling_constraints_doc['sky']['transit_offset'] = {'from': -12 * 60 * 60, 'to': 12 * 60 * 60} + su_draft.scheduling_constraints_doc['sky']['min_elevation'] = {'target': 0, 'calibrator': 0} + su_draft.scheduling_constraints_doc['sky']['min_distance'] = {'sun': 0, 'moon': 0, 'jupiter': 0} + su_draft.save() + + su_blueprint = create_scheduling_unit_blueprint_and_tasks_and_subtasks_from_scheduling_unit_draft(su_draft) # start the dynamic_scheduling_service, which includes eventmessage handling, and a running scheduler with BusListenerJanitor(create_dynamic_scheduling_service()): # at scheduler startup, all units are evaluated for schedulability. # assert the scheduling_unit has been marked as unschedulable - su_blueprint = create_scheduling_unit_blueprint_and_tasks_and_subtasks_from_scheduling_unit_draft(su_draft) - - # now wait and poll until unit is unschedulable, or timeout + # wait and poll until unit is unschedulable, or timeout su_blueprint = wait_for_scheduling_unit_blueprint_status(su_blueprint.id, models.SchedulingUnitStatus.Choices.UNSCHEDULABLE.value, timeout=60) self.assertEqual(models.SchedulingUnitStatus.Choices.UNSCHEDULABLE.value, su_blueprint.status.value) # all subtasks should have a unschedulable_reason self.assertTrue(all([s.unschedulable_reason is not None for s in su_blueprint.subtasks.all()])) - self.assertTrue(all([s.unschedulable_reason == 'project is not active' for s in su_blueprint.subtasks.filter(specifications_template__type__value='observation').all()])) + self.assertTrue(all([s.unschedulable_reason == ('project %s is not active'%(project.name,)) for s in su_blueprint.subtasks.filter(specifications_template__type__value='observation').all()])) self.assertTrue(all([s.unschedulable_reason == 'predecessor is unschedulable' for s in su_blueprint.subtasks.exclude(specifications_template__type__value='observation').all()])) # check tasks and unit as well @@ -2171,11 +2177,11 @@ class TestDynamicScheduling(BaseDynamicSchedulingTestCase): best_scored_scheduling_unit = self.scheduler.find_best_next_schedulable_unit([su_blueprint], datetime(2023, 2, 16, 23, 0), datetime(2023, 2, 17, 11, 0)) self.assertIsNone(best_scored_scheduling_unit) - # now determine the unschedulable reason..., and check that it is indeed incorrect as the bug reports. - su_blueprint = determine_unschedulable_reason_and_mark_unschedulable_if_needed(su_blueprint, datetime(2023, 2, 16, 23, 0), datetime(2023, 2, 17, 11, 0), gridder=self.scheduler.search_gridder) + # now determine the unschedulable reason..., and check that it now correct after the bugfix. + su_blueprint = determine_unschedulable_reason_and_mark_unschedulable_if_needed(su_blueprint, datetime(2023, 2, 16, 23, 0), datetime(2023, 2, 17, 11, 0), gridder=self.scheduler.fine_gridder) self.assertEqual(models.SchedulingUnitStatus.Choices.UNSCHEDULABLE.value, su_blueprint.status.value) logger.info(su_blueprint.unschedulable_reason) - # confirming bug: FR606 is falsely in the reason, and IE613 is not. + # bug fixed?: FR606 should not be in reason anymore, and IE613 should. self.assertFalse("FR606" in su_blueprint.unschedulable_reason) self.assertTrue("IE613" in su_blueprint.unschedulable_reason) -- GitLab