diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py b/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py index 142c1635f1cc1207c7700f525593b635de6eb939..8292f5d6c79ee372e9e27d660e22a6d77e607ed7 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py @@ -34,21 +34,22 @@ SUN_SET_RISE_ANGLE_TO_HORIZON = Angle(10, unit=astropy.units.deg) SUN_SET_RISE_PRECISION = 30 -@lru_cache(maxsize=256, typed=False) # does not like lists, so use tuples to allow caching def timestamps_and_stations_to_sun_rise_and_set(timestamps: tuple, stations: tuple, angle_to_horizon: Angle=SUN_SET_RISE_ANGLE_TO_HORIZON, create_when_not_found=False) -> dict: """ Retrieve for given stations and given timestamps the sunrise/sunset/day/night data as dictionary If station/timestamp is already calculated it will be retrieved from database otherwise it will be calculated - and added to the database for possible future retrieval (optional). + and added to the database for possible future retrieval (optional parameter must be true). Storing the pre-calculated data into a database makes retrieval faster. - TODO make a service to pre-calculate e.g. 1 year in advanced, currently only one week for all stations - during populate of testenvironment - TODO about the night.... when using a time in the timestamp + + The day/sunrise/sunset is always on the date of the timestamp. + The night is usually the one _starting_ on the date of the time stamp, unless the given timestamp falls before + sunrise, in which case it is the night _ending_ on the timestamp date. + :param timestamps: tuple of datetimes, e.g. datetime(2020, 1, 1) :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 - :param: create_when_not_found: Add data to database if not found and so calculated for first time + :param: create_when_not_found: Add data to database if not found in database and so calculated for first time :return A dict that maps station names to a nested dict that contains lists of start and end times for sunrise, sunset, day and night, on each requested date. E.g. @@ -66,6 +67,7 @@ def timestamps_and_stations_to_sun_rise_and_set(timestamps: tuple, stations: tup """ return_dict = {} for station in stations: + observer = create_astroplan_observer_for_station(station) for timestamp in timestamps: # We can also check if ALL stations/timestamps are in DB once. Do it now in a loop for each # station/timestamp, because we might missing something @@ -81,12 +83,18 @@ def timestamps_and_stations_to_sun_rise_and_set(timestamps: tuple, stations: tup if station_timestamp_found: logger.debug("StationTimeline data found in DB for station=%s, timestamp=%s" % (station,timestamp)) sunrise_dict = {"start": obj.sunrise_start, "end": obj.sunrise_end} - sunset_dict = {"start": obj.sunset_start, "end": obj.sunset_start} - day_dict = {"start": obj.day_start, "end": obj.day_end} - night_dict = {"start": obj.night_start, "end": obj.night_end} + sunset_dict = {"start": obj.sunset_start, "end": obj.sunset_end} else: - observer = create_astroplan_observer_for_station(station) - sunrise_dict, sunset_dict, day_dict, night_dict = get_sunrise_and_sunset_of_observer_day(observer, timestamp, angle_to_horizon) + # Not found in database so calculate it + try: + sunrise_dict, sunset_dict = calculate_and_get_sunrise_and_sunset_of_observer_day(observer, timestamp, angle_to_horizon) + except Exception as exp: + logger.warning("Can not calculate sunrise/sunset for station=%s, timestamp=%s" % (station,timestamp)) + # raise exp + # Don't let it crash for now + # The stations SE607 and LV614 station has problems calculation on 2021-07-01.... + # The SE607 also on 2021-06-04 ?? + break # Add to database if create_when_not_found: station_timeline = StationTimeline.objects.create( @@ -95,14 +103,37 @@ def timestamps_and_stations_to_sun_rise_and_set(timestamps: tuple, stations: tup sunrise_start=sunrise_dict['start'], sunrise_end=sunrise_dict['end'], sunset_start=sunset_dict['start'], - sunset_end=sunset_dict['end'], - day_start=day_dict['start'], - day_end=day_dict['end'], - night_start=night_dict['start'], - night_end=night_dict['end']) + sunset_end=sunset_dict['end']) logger.debug("StationTimeline %s calculated and created for station=%s, timestamp=%s" % (station_timeline, station, timestamp)) - # Create json overall result + + # Derive day/night from sunset/sunrise + day_dict = {"start": sunrise_dict["end"], "end": sunset_dict["start"]} + + if timestamp >= sunrise_dict["start"]: + # Determine next sunrise start + try: + obj_next = StationTimeline.objects.get(station_name=station, + timestamp=datetime.date(timestamp + timedelta(days=1))) + sunrise_next_start = obj_next.sunrise_start + except: + sunrise_next_start = observer.sun_rise_time(time=Time(sunrise_dict["end"]), horizon=-angle_to_horizon, + which='next', + n_grid_points=SUN_SET_RISE_PRECISION).to_datetime() + night_dict = {"start": sunset_dict["end"], "end": sunrise_next_start} + else: + # Determine previous sunset end + try: + obj_prev = StationTimeline.objects.get(station_name=station, + timestamp=datetime.date(timestamp - timedelta(days=1))) + sunset_previous_end = obj_prev.sunrise_start + except: + sunset_previous_end = observer.sun_set_time(time=Time(sunrise_dict["start"]), horizon=-angle_to_horizon, + which='previous', + n_grid_points=SUN_SET_RISE_PRECISION).to_datetime() + night_dict = {"start": sunset_previous_end, "end": sunrise_dict["start"]} + + # Create overall result return_dict.setdefault(station, {}) return_dict[station].setdefault("sunrise", []).append(sunrise_dict) return_dict[station].setdefault("sunset", []).append(sunset_dict) @@ -113,16 +144,13 @@ def timestamps_and_stations_to_sun_rise_and_set(timestamps: tuple, stations: tup @lru_cache(maxsize=256, typed=False) -def get_sunrise_and_sunset_of_observer_day(observer, timestamp: datetime, angle_to_horizon: Angle) -> dict: +def calculate_and_get_sunrise_and_sunset_of_observer_day(observer, timestamp: datetime, angle_to_horizon: Angle) -> dict: """ - Compute sunrise, sunset, day and night of the given observer object (station) at the given timestamp. - The day/sunrise/sunset is always on the date of the timestamp. - The night is usually the one _starting_ on the date of the time stamp, unless the given timestamp falls before - sunrise, in which case it is the night _ending_ on the timestamp date. + Compute sunrise, sunset of the given observer object (station) at the given timestamp. :param observer: observer object :param timestamp: Datetime of a day (datetime(2020, 1, 1) :param the angle between horizon and given coordinates for which rise and set times are returned - :return: dictionaries (with 'start' and 'end' defined) of sunrise, sunset, day, night + :return: dictionaries (with 'start' and 'end' defined) of sunrise, sunset """ sunrise_start = observer.sun_rise_time(time=Time(datetime.combine(timestamp.date(), dtime(12, 0, 0))), horizon=-angle_to_horizon, which='previous', @@ -136,17 +164,8 @@ def get_sunrise_and_sunset_of_observer_day(observer, timestamp: datetime, angle_ sunrise_dict = {"start": sunrise_start.to_datetime(), "end": sunrise_end.to_datetime()} sunset_dict = {"start": sunset_start.to_datetime(), "end": sunset_end.to_datetime()} - day_dict = {"start": sunrise_end.to_datetime(), "end": sunset_start.to_datetime()} - if timestamp >= sunrise_start: - sunrise_next_start = observer.sun_rise_time(time=sunset_end, horizon=-angle_to_horizon, which='next', - n_grid_points=SUN_SET_RISE_PRECISION) - night_dict = {"start": sunset_end.to_datetime(), "end": sunrise_next_start.to_datetime()} - else: - sunset_previous_end = observer.sun_set_time(time=sunrise_start, horizon=-angle_to_horizon, which='previous', - n_grid_points=SUN_SET_RISE_PRECISION) - night_dict = {"start": sunset_previous_end.to_datetime(), "end": sunrise_start.to_datetime()} - return sunrise_dict, sunset_dict, day_dict, night_dict + return sunrise_dict, sunset_dict # todo: Depending on usage patterns, we should consider refactoring this a little so that we cache on a function with a single timestamp as input. Requests with similar (but not identical) timestamps or bodies currently make no use of cached results for the subset computed in previous requests. @@ -289,21 +308,14 @@ def get_all_stations(): returns all possible stations. Retrieve station names from station template by getting the Dutch and International stations, then you should have it all. - lst_stations = ["CS001", "CS002", "CS003", "CS004", "CS005", "CS006", "CS007", "CS011", "CS013", "CS017", "CS021", - "CS024", "CS026", "CS028", "CS030", "CS031", "CS032", "CS101", "CS103", "CS201", "CS301", "CS302", - "CS401", "CS501", - "RS106", "RS205", "RS208", "RS210", "RS305", "RS306", "RS307", "RS310", "RS406", "RS407", "RS409", - "RS503", "RS508", "RS509", - "DE601", "DE602", "DE603", "DE604", "DE605", "FR606", "SE607", "UK608", "DE609", "PL610", "PL611", - "PL612", "IE613", "LV614"] """ lst_stations = [] for station_group in ["Dutch", "International"]: - station_schema_template = CommonSchemaTemplate.objects.get(name="stations", version=1) - groups = station_schema_template.schema['definitions']['station_group']['anyOf'] try: + station_schema_template = CommonSchemaTemplate.objects.get(name="stations", version=1) + groups = station_schema_template.schema['definitions']['station_group']['anyOf'] selected_group = next(g for g in groups if g['title'].lower() == station_group.lower()) - except StopIteration: - raise ValueError('No station_group with name "%s" found in the JSON schema.' % station_group) - lst_stations.extend(selected_group['properties']['stations']['enum'][0]) + lst_stations.extend(selected_group['properties']['stations']['enum'][0]) + except Exception: + logger.warning("No stations schema found, sorry can not determine station list, return empty list") return lst_stations diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py index 8a62e2862a4f83e2dac48a78fdf30406aa19fc15..52324656a177f74bb4c5467e9c94c689962a7994 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py @@ -904,10 +904,6 @@ class Migration(migrations.Migration): ('sunrise_end', models.DateTimeField(null=True, help_text='End time of the sunrise.')), ('sunset_start', models.DateTimeField(null=True, help_text='Start time of the sunset.')), ('sunset_end', models.DateTimeField(null=True, help_text='End time of the sunset.')), - ('day_start', models.DateTimeField(null=True, help_text='Start time of the day.')), - ('day_end', models.DateTimeField(null=True, help_text='End time of the day.')), - ('night_start', models.DateTimeField(null=True, help_text='Start time of the night.')), - ('night_end', models.DateTimeField(null=True, help_text='End time of the night.')) ], ), migrations.AddConstraint( diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/calculations.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/calculations.py index b1b233acb4fa2c036b7f77a6b7fcf3699a1ba84a..3905cfeae06892743ba39bee500dce227e6e79e5 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/calculations.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/calculations.py @@ -11,7 +11,8 @@ from django.db.models import Model, CharField, DateTimeField, DateField class StationTimeline(Model): """ - Represents computations of sunrise, sunset, day and night of the given stations at the given timestamps. + Represents computations of sunrise, sunset of the given stations at the given timestamps. + Day and night are derived from sunset/sunrise data. The day/sunrise/sunset is always on the date of the timestamp. The night is usually the one _starting_ on the date of the time stamp, unless the given timestamp falls before sunrise, in which case it is the night _ending_ on the timestamp date. @@ -23,8 +24,5 @@ class StationTimeline(Model): sunrise_end = DateTimeField(null=True, help_text='End time of the sunrise.') sunset_start = DateTimeField(null=True, help_text='Start time of the sunset.') sunset_end = DateTimeField(null=True, help_text='End time of the sunset.') - day_start = DateTimeField(null=True, help_text='Start time of the day.') - day_end = DateTimeField(null=True, help_text='End time of the day.') - night_start = DateTimeField(null=True, help_text='Start time of the night.') - night_end = DateTimeField(null=True, help_text='End time of the night.') + diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py index 18046daac1d7c76c09c8ab8e05341b0758a39b93..3b3147b6774851a059d16e46b9454f2559ff6db5 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py @@ -69,7 +69,7 @@ def populate_test_data(): from lofar.common.json_utils import get_default_json_object_for_schema # Maybe move to 'migrations populate' - populate_calculations(nbr_days=3) + #populate_calculations(nbr_days=3) constraints_template = models.SchedulingConstraintsTemplate.objects.get(name="constraints") constraints_spec = get_default_json_object_for_schema(constraints_template.schema) @@ -485,18 +485,17 @@ def populate_system_test_users(): lta_user.groups.add(Group.objects.get(name='LTA User')) -def populate_calculations(nbr_days=3): +def populate_calculations(nbr_days=3, start_date=date.today()): """ - Calculate a week of station timeline data of all stations - will take about a minute - TODO create a service which will do this continuously ? + Populate station timeline data of all stations for given number of days the starting at given date + Note: If data is not in database yet, it will take about 6 seconds to calculate it for all (51) stations """ - starttime = datetime.utcnow() - logger.info("Populate sunrise, sunset, day, night for ALL stations from today up to %d days" % nbr_days) + starttime_for_logging = datetime.utcnow() + logger.info("Populate sunrise and sunset for ALL known stations from %s up to %d days" % (start_date, nbr_days)) lst_timestamps = [] for i in range(0, nbr_days): - dt = datetime.combine(date.today(), datetime.min.time()) + timedelta(days=i) + dt = datetime.combine(start_date, datetime.min.time()) + timedelta(days=i) lst_timestamps.append(dt) timestamps_and_stations_to_sun_rise_and_set(tuple(lst_timestamps), tuple(get_all_stations()), create_when_not_found=True) - logger.info("Done in %.1fs", (datetime.utcnow()-starttime).total_seconds()) + logger.info("Done in %.1fs", (datetime.utcnow()-starttime_for_logging).total_seconds()) diff --git a/SAS/TMSS/backend/test/test_utils.py b/SAS/TMSS/backend/test/test_utils.py index ad64349b5c195fb1d7b5381cc2a4404a535711ae..9f6b7c1889f8359a39e60f38a779c4a86df891f4 100644 --- a/SAS/TMSS/backend/test/test_utils.py +++ b/SAS/TMSS/backend/test/test_utils.py @@ -585,6 +585,7 @@ def main_test_environment(): group.add_option('-f', '--feedbackservice', dest='feedbackservice', action='store_true', help='Enable feedbackservice to handle feedback from observations/pipelines which comes in via the (old qpid) otdb messagebus.') group.add_option('--all', dest='all', action='store_true', help='Enable/Start all the services, upload schemas and testdata') group.add_option('--simulate', dest='simulate', action='store_true', help='Simulate a run of the first example scheduling_unit (implies --data and --eventmessages and --ra_test_environment)') + group.add_option('--calculation_service', dest='calculation_service', action='store_true', help='Enable the (Pre-)Calculations service') group = OptionGroup(parser, 'Messaging options') parser.add_option_group(group) @@ -656,6 +657,16 @@ def main_test_environment(): except KeyboardInterrupt: return + # This is just a 'simple' timing service + if options.calculation_service: + stop_event = threading.Event() + create_calculation_service(stop_event=stop_event) + try: + stop_event.wait() + except KeyboardInterrupt: + print("KeyboardInterrupt") + return + waitForInterrupt() @@ -958,5 +969,71 @@ def main_scheduling_unit_blueprint_simulator(): pass +def create_calculation_service(stop_event: threading.Event): + """ + First implementation of a simple time scheduler, starting with one major task and then run once a day + It created and start the populate of sunset/sunrise calculations + Should be organised a little bit differently and better!!!!!!! + """ + print("create_calculation_service") + + import threading, time, signal + from datetime import timedelta + # Import here otherwise you get + # "django.core.exceptions.ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings." + from lofar.sas.tmss.tmss.tmssapp.populate import populate_calculations + + # Some parameters, currently as constants + WAIT_TIME_SECONDS = 300 + NBR_DAYS_CALCULATE_AHEAD = 100 + NBR_DAYS_BEFORE_TODAY = 1 + + + class ProgramKilled(Exception): + pass + + def populate_calculations_once_a_day(): + days_offset = NBR_DAYS_CALCULATE_AHEAD - NBR_DAYS_BEFORE_TODAY + populate_calculations(nbr_days=1, start_date=datetime.date.today() + datetime.timedelta(days=days_offset)) + + def signal_handler(signum, frame): + raise ProgramKilled + + class Job(threading.Thread): + def __init__(self, interval, execute, *args, **kwargs): + threading.Thread.__init__(self) + self.daemon = False + self.stopped = threading.Event() + self.interval = interval + self.execute = execute + self.args = args + self.kwargs = kwargs + + def stop(self): + self.stopped.set() + self.join() + + def run(self): + while not self.stopped.wait(self.interval.total_seconds()): + self.execute(*self.args, **self.kwargs) + + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + job = Job(interval=timedelta(seconds=WAIT_TIME_SECONDS), execute=populate_calculations_once_a_day) + job.start() + + # Start one day before today + populate_calculations(nbr_days=NBR_DAYS_CALCULATE_AHEAD, start_date=datetime.date.today() - datetime.timedelta(days=NBR_DAYS_BEFORE_TODAY)) + + print("Execute Every %d sec" % WAIT_TIME_SECONDS ) + while True: + try: + time.sleep(10) + except ProgramKilled: + print("Program killed: running cleanup code") + job.stop() + break + + if __name__ == '__main__': main_test_environment() diff --git a/SAS/TMSS/client/lib/tmss_http_rest_client.py b/SAS/TMSS/client/lib/tmss_http_rest_client.py index bcd7309b82b976177ce4f527435e2bea60b9cb09..8ca49cf4cbd16802330bcf504e21156298aff771 100644 --- a/SAS/TMSS/client/lib/tmss_http_rest_client.py +++ b/SAS/TMSS/client/lib/tmss_http_rest_client.py @@ -371,7 +371,7 @@ class TMSSsession(object): if response.status_code == 201: logger.info("created new template with name=%s: %s", name, json.loads(response.text)['url']) else: - raise Exception("Could not POST template with name=%s: %s" (name,response.text)) + raise Exception("Could not POST template with name=%s: %s" % (name,response.text)) def process_feedback_and_set_to_finished_if_complete(self, subtask_id: int, feedback: str) -> {}: '''Process the feedback_doc (which can be for one or more or all dataproducts), store/append it in the subtask's raw_feedback, and process it into json feedback per dataproduct. Sets the subtask to finished if all dataproducts are processed, which may require multiple postings of partial feedback docs.