From f855ec7537b2deb5f96e00f8b09f302bcf5e975b Mon Sep 17 00:00:00 2001
From: Jorrit Schaap <schaap@astron.nl>
Date: Mon, 12 Jun 2023 16:56:25 +0200
Subject: [PATCH] TMSS-2582: bonus, performance boost. early exit when a unit
 has no earliest possible starttime.

---
 .../services/scheduling/lib/constraints.py    | 45 ++++++++++++-------
 1 file changed, 30 insertions(+), 15 deletions(-)

diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints.py b/SAS/TMSS/backend/services/scheduling/lib/constraints.py
index 3445b7d87a9..e229533c8b1 100644
--- a/SAS/TMSS/backend/services/scheduling/lib/constraints.py
+++ b/SAS/TMSS/backend/services/scheduling/lib/constraints.py
@@ -833,6 +833,7 @@ def evaluate_sky_transit_constraint(scheduling_unit: models.SchedulingUnitBluepr
     transit_from_limit_with_margin = transit_from_limit - 60*gridder.grid_minutes
     transit_to_limit_with_margin   = transit_to_limit   + 60*gridder.grid_minutes
 
+    logger.debug("evaluate_sky_transit_constraint: SUB id=%s proposed_start_time='%s'", scheduling_unit.id, proposed_start_time)
 
     # transits are only computed for target observations
     target_obs_tasks = [t for t in scheduling_unit.observation_tasks if t.is_target_observation]
@@ -849,7 +850,7 @@ def evaluate_sky_transit_constraint(scheduling_unit: models.SchedulingUnitBluepr
         # since the constraint only applies to the middle of the obs, only consider the proposed_center_time
         # take along the relative_start_time of this task compared to the scheduling unit's start_time
         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)
+        task_proposed_center_time = round_to_second_precision(task_proposed_start_time + (target_obs_task.specified_duration / 2))
 
         stations = get_schedulable_stations(target_obs_task, task_proposed_start_time)
         stations = get_unique_sorted_boundary_stations_and_cs002(stations, 50e4)
@@ -864,7 +865,9 @@ def evaluate_sky_transit_constraint(scheduling_unit: models.SchedulingUnitBluepr
             for station, transit_timestamps in transits.items():
                 assert len(transit_timestamps) == 1 # only one center time
                 transit_timestamp = round_to_second_precision(transit_timestamps[0])
-                logger.debug("SUB id=%s transit='%s' for %s %s", scheduling_unit.id, transit_timestamp, station, pointing)
+                if logger.level==logging.DEBUG:
+                    transit_timestamp_lst = local_sidereal_time_for_utc_and_station(transit_timestamp, station)
+                    logger.debug("SUB id=%s transit='%sUTC' '%sLST' for %s %s task_proposed_center_time='%s'", scheduling_unit.id, transit_timestamp, transit_timestamp_lst, station, pointing.str_astro(), task_proposed_center_time)
 
                 # transit minus half duration is by definition the optimal start_time
                 # also take the task relative start time against the su.starttime into account
@@ -1209,7 +1212,8 @@ def get_earliest_possible_start_time_for_sky_min_elevation(scheduling_unit: mode
         logger.debug('get_earliest_possible_start_time_for_sky_min_elevation %s', result)
 
         if not result.has_constraint:
-            return None
+            # if there is no constraint, the earliest_possible_start_time is just right away, at the lower_bound
+            return lower_bound
 
         if result.is_constraint_met:
             return result.earliest_possible_start_time
@@ -1252,14 +1256,15 @@ def get_earliest_possible_start_time_for_sky_transit_offset(scheduling_unit: mod
         logger.debug('get_earliest_possible_start_time_for_sky_transit_offset %s', result)
 
         if not result.has_constraint:
-            return None
+            # if there is no constraint, the earliest_possible_start_time is just right away, at the lower_bound
+            return lower_bound
 
         if result.is_constraint_met:
             if result.earliest_possible_start_time > lower_bound:
                 return result.earliest_possible_start_time
             return lower_bound
 
-        # constraint is not met, or before lower_bound... or equal to previous evaulation result
+        # constraint is not met, or before lower_bound... or equal to previous evaluation result
         if result.earliest_possible_start_time is not None and result.earliest_possible_start_time >= lower_bound and allow_quick_jump:
             # quick jump to earliest_possible_start_time and evaluate to confirm that constraint is met.
             possible_start_time = result.earliest_possible_start_time
@@ -1290,7 +1295,8 @@ def get_earliest_possible_start_time_for_sky_min_distance(scheduling_unit: model
         logger.debug('get_earliest_possible_start_time_for_sky_min_distance %s', result)
 
         if not result.has_constraint:
-            return None
+            # if there is no constraint, the earliest_possible_start_time is just right away, at the lower_bound
+            return lower_bound
 
         if result.is_constraint_met:
             return result.earliest_possible_start_time
@@ -1505,7 +1511,8 @@ def get_earliest_possible_start_time_for_daily_constraints(scheduling_unit: mode
         logger.debug('get_earliest_possible_start_time_for_daily_constraints %s', result)
 
         if not result.has_constraint:
-            return None
+            # if there is no constraint, the earliest_possible_start_time is just right away, at the lower_bound
+            return lower_bound
 
         if not result.is_constraint_met and result.earliest_possible_start_time is not None:
             # advance straight to earliest_possible_start_time, and evaluate again to ensure the constraint is met
@@ -1560,6 +1567,7 @@ def get_earliest_possible_start_time_for_time_constraints(scheduling_unit: model
             from_timestamps = [parser.parse(between["from"], ignoretz=True)
                                for between in constraints['time']['between']
                                if lower_bound is None or parser.parse(between["to"], ignoretz=True) > lower_bound]
+            from_timestamps = [max(lower_bound, ts) for ts in from_timestamps]
             if from_timestamps:
                 earliest_possible_start_times.add(min(from_timestamps))
 
@@ -1585,8 +1593,8 @@ def get_earliest_possible_start_time_for_time_constraints(scheduling_unit: model
     if not earliest_possible_start_times and not (constraints.get('time', {}).get('at') or
                                                   constraints.get('time', {}).get('before') or
                                                   constraints.get('time', {}).get('after') or
-                                                  constraints.get('time',{}).get('between') or
-                                                  constraints.get('time',{}).get('not_between')):
+                                                  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
 
@@ -1594,7 +1602,8 @@ def get_earliest_possible_start_time_for_time_constraints(scheduling_unit: model
         earliest_possible_start_times = [t for t in earliest_possible_start_times if t >= lower_bound]
 
     if upper_bound is not None:
-        earliest_possible_start_times = [t for t in earliest_possible_start_times if t <= upper_bound]
+        start_before = upper_bound - scheduling_unit.specified_observation_duration
+        earliest_possible_start_times = [t for t in earliest_possible_start_times if t <= start_before]
 
     if earliest_possible_start_times:
         return max(earliest_possible_start_times)
@@ -1637,8 +1646,11 @@ def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBluep
                                                     get_earliest_possible_start_time_for_sky_min_distance):
         try:
             earliest_possible_start_time = get_earliest_possible_start_time_method(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped)
-            if earliest_possible_start_time is not None:
-                earliest_possible_start_times.add(earliest_possible_start_time)
+            if earliest_possible_start_time is None:
+                # early exit. None result means no possible starttime at all for this unit with these constraints in this time window.
+                logger.debug("get_earliest_possible_start_time SUB id=%s window=['%s', '%s'] early exit returning None, because %s returned None", scheduling_unit.id, lower_bound, upper_bound, get_earliest_possible_start_time_method.__name__)
+                return None
+            earliest_possible_start_times.add(earliest_possible_start_time)
         except SchedulerInterruptedException:
             raise
         except Exception as e:
@@ -1647,13 +1659,15 @@ def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBluep
     if len(earliest_possible_start_times) == 0:
         # it's possible that none of the above constraints yielded an earliest_possible_start_time (or that there are no constraints)
         # this might mean that the unit can start right away at the lower_bound
-        # so, always add lower_bound, and evaluate it below (along with the other possible earliest_possible_start_times) if it can actually run.
-        earliest_possible_start_times.add(lower_bound)
+        # so, add lower_bound if the unit can directly run at lower_bound.
+        if can_run_at(scheduling_unit, lower_bound, gridder):
+            earliest_possible_start_times.add(lower_bound)
 
     # filter for non-None and within bound(s)
     earliest_possible_start_times = set([t for t in earliest_possible_start_times if t is not None and t >= lower_bound])
     if upper_bound is not None:
-        earliest_possible_start_times = set([t for t in earliest_possible_start_times if t <= upper_bound])
+        start_before = upper_bound - scheduling_unit.specified_observation_duration
+        earliest_possible_start_times = set([t for t in earliest_possible_start_times if t <= start_before])
     logger.debug("get_earliest_possible_start_time SUB id=%s lower_bound='%s' earliest_possible_start_times per constraint: %s", scheduling_unit.id, lower_bound, ', '.join([str(t) for t in sorted(earliest_possible_start_times)]))
 
     # the earliest_possible_start_times were computed per constraint-type.
@@ -1887,6 +1901,7 @@ def get_min_earliest_possible_start_time(scheduling_units: [models.SchedulingUni
             if earliest_possible_start_time is not None:
                 if min_earliest_possible_start_time is None or earliest_possible_start_time < min_earliest_possible_start_time:
                     min_earliest_possible_start_time = earliest_possible_start_time
+    logger.debug("get_min_earliest_possible_start_time returning '%s' for window=['%s', '%s'] and unit_ids=%s", min_earliest_possible_start_time, lower_bound, upper_bound, ','.join(str(s.id) for s in scheduling_units))
     return min_earliest_possible_start_time
 
 
-- 
GitLab