Skip to content

Commit

Permalink
Merge pull request #306 from observatorycontrolsystem/fix/more_realti…
Browse files Browse the repository at this point in the history
…me_restrictions

Fix/more realtime restrictions
  • Loading branch information
jnation3406 authored Aug 28, 2024
2 parents 01ce24d + 66c14d6 commit 74ed473
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ jobs:
python -m pip install --upgrade pip
python -m pip install 'poetry'
poetry install
env:
SETUPTOOLS_USE_DISTUTILS: stdlib
- name: Run tests
run: |
poetry run coverage run manage.py test --settings=observation_portal.test_settings
Expand Down Expand Up @@ -88,6 +90,8 @@ jobs:
poetry build -f wheel
poetry build -f sdist
poetry publish -u "__token__" -p '${{ secrets.PYPI_OBS_PORTAL_API_TOKEN }}'
env:
SETUPTOOLS_USE_DISTUTILS: stdlib


build_and_publish_image:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/docs-gh-pages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
python -m pip install --upgrade pip
python -m pip install 'poetry'
poetry install
env:
SETUPTOOLS_USE_DISTUTILS: stdlib
- name: Build docs
run: |
poetry run python manage.py generateschema_mocked --file observation-portal.yaml --generator_class=observation_portal.common.schema.ObservationPortalSchemaGenerator
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ COPY ./README.md ./pyproject.toml ./poetry.lock ./
# NOTE: pySLALIB's setup.py is messed up as it requires numpy to already be
# installed to install it. https://github.com/scottransom/pyslalib/blob/fcb0650a140a8002cc6c0e8918c3e4c6fe3f8e01/setup.py#L3
# So please excuse the ugly hack.

ENV SETUPTOOLS_USE_DISTUTILS=stdlib
RUN pip install -r <(poetry export | grep "numpy") && \
pip install -r <(poetry export)

Expand Down
96 changes: 95 additions & 1 deletion observation_portal/observations/realtime.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,92 @@
from datetime import timedelta
from datetime import timedelta, datetime
from collections import defaultdict

from django.conf import settings
from django.utils import timezone
from django.contrib.auth.models import User
from time_intervals.intervals import Intervals

from observation_portal.proposals.models import Semester
from observation_portal.observations.models import Observation
from observation_portal.requestgroups.models import RequestGroup
from observation_portal.common.configdb import configdb
from observation_portal.common.rise_set_utils import filtered_dark_intervalset_for_telescope


def get_already_booked_intervals(user: User, start: datetime, end: datetime):
""" Get already booked times for this user to block out on all telescopes
"""
already_booked_obs = Observation.objects.filter(
request__request_group__submitter=user,
request__request_group__observation_type=RequestGroup.REAL_TIME,
start__lt=end,
end__gt=start
)
already_booked_intervals = []
for obs in already_booked_obs:
already_booked_intervals.append((obs.start, obs.end))
return Intervals(already_booked_intervals)


def obs_query_to_intervals_by_resource(obs_query):
""" Takes in a django queryset of Observations and returns a dict of Intervals by resource for those observations
"""
intervals_by_resource = defaultdict(list)
# Iterate over all observations and add their start/end times to tuple list per resource
for obs in obs_query:
resource = f'{obs.telescope}.{obs.enclosure}.{obs.site}'
intervals_by_resource[resource].append((obs.start, obs.end))

# Now iterate over all interval tuple lists and convert them to interval objects
for resource in intervals_by_resource.keys():
intervals_by_resource[resource] = Intervals(intervals_by_resource[resource])

return intervals_by_resource


def get_in_progress_observation_intervals_by_resource(telescope_filter: str, start: datetime, end: datetime):
""" Get a dict of in progress observation intervals of time to block off by resource (tel.enc.site)
"""
# First get the set of currently running observations in the time interval
running_obs = Observation.objects.filter(
state='IN_PROGRESS',
start__lt=end,
end__gt=start,
)
# If a specific telescope_filter is specified, we can restrict the query here
if telescope_filter:
telescope, enclosure, site = telescope_filter.split('.')
running_obs.filter(
telescope=telescope,
enclosure=enclosure,
site=site
)

return obs_query_to_intervals_by_resource(running_obs)


def get_future_important_scheduled_observation_intervals_by_resource(telescope_filter: str, start: datetime, end: datetime):
""" Get a dict of future observation intervals of time to block off by resource (tel.enc.site)
"""
# First get the set of future TC, RR, and Direct observations in the time interval
future_important_obs = Observation.objects.filter(
request__request_group__observation_type__in=[
RequestGroup.TIME_CRITICAL, RequestGroup.RAPID_RESPONSE, RequestGroup.DIRECT],
start__lt=end,
end__gt=start,
)
# If a specific telescope_filter is specified, we can restrict the query here
if telescope_filter:
telescope, enclosure, site = telescope_filter.split('.')
future_important_obs.filter(
telescope=telescope,
enclosure=enclosure,
site=site
)

return obs_query_to_intervals_by_resource(future_important_obs)


def get_realtime_availability(user: User, telescope_filter: str = ''):
""" Returns a dict of lists of availability intervals for each
telescope, limited to just a specific telescope if specified
Expand All @@ -31,9 +109,25 @@ def get_realtime_availability(user: User, telescope_filter: str = ''):
# Now go through each telescope and get its availability intervals
start = timezone.now() + timedelta(minutes=settings.REAL_TIME_AVAILABILITY_MINUTES_IN)
end = start + timedelta(days=settings.REAL_TIME_AVAILABILITY_DAYS_OUT)

# Get already booked times for this user to block out on all telescopes
already_booked_interval = get_already_booked_intervals(user, start, end)
# Get in progress observation intervals by resource
in_progress_intervals = get_in_progress_observation_intervals_by_resource(telescope_filter, start, end)
# Get future scheduled TC, RR, and Direct observation intervals by resource
future_intervals = get_future_important_scheduled_observation_intervals_by_resource(telescope_filter, start, end)

for resource in telescopes_availability.keys():
telescope, enclosure, site = resource.split('.')
intervals = filtered_dark_intervalset_for_telescope(start, end, site, enclosure, telescope)
# Now also filter out running obs, future high priority (TC, RR, Direct) obs, and obs overlapping in time by the user
intervals_to_block = []
if resource in in_progress_intervals:
intervals_to_block.append(in_progress_intervals[resource])
if resource in future_intervals:
intervals_to_block.append(future_intervals[resource])
intervals_to_block = already_booked_interval.union(intervals_to_block)
intervals = intervals.subtract(intervals_to_block)
for interval in intervals.toTupleList():
telescopes_availability[resource].append([interval[0].isoformat(), interval[1].isoformat()])

Expand Down
58 changes: 58 additions & 0 deletions observation_portal/observations/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from datetime import datetime, timedelta

from rest_framework import serializers
from django.utils import timezone
from django.db import transaction
from django.utils.translation import gettext as _
from django.utils.module_loading import import_string
from django.conf import settings
from django.contrib.auth.models import User

from observation_portal.common.configdb import configdb
from observation_portal.common.rise_set_utils import is_interval_available_for_telescope
Expand All @@ -20,6 +23,51 @@
logger = logging.getLogger()


def realtime_interval_availability_checks(user: User, start: datetime, end: datetime, site: str, enclosure: str, telescope: str):
""" Performs a few different availability checks on a given interval - makes sure it does not overlap with another realtime session
for the user, or that it doesn't overlap with a currently running observation or scheduled TC, RR, or Direct observation
raises ValidationError if any check fails
"""
# First check if the user has a realtime observation that is overlapping in time
overlapping_obs = Observation.objects.filter(
request__request_group__submitter=user,
request__request_group__observation_type=RequestGroup.REAL_TIME,
start__lt=end,
end__gt=start)
if overlapping_obs.count() > 0:
raise serializers.ValidationError(_(f"The desired interval of {start} to {end} overlaps an existing interval for user {user.username}"))
# Now check if there is a in progress observation of any type on the telescope at this time
# If the start time is > 8 hours in the future we don't even need to check this - no obs are longer than 8 hours
if start < (timezone.now() + timedelta(hours=8)):
running_obs = Observation.objects.filter(
state='IN_PROGRESS',
start__lt=end,
end__gt=start,
site=site,
enclosure=enclosure,
telescope=telescope
).first()
if running_obs:
raise serializers.ValidationError(_(
f"""There is currently an observation in progress on {site}.{
enclosure}.{telescope} that would overlap with your session."""
))
# Now check if there are future scheduled observations that overlap and are TC, RR, or Direct type
future_important_obs = Observation.objects.filter(
request__request_group__observation_type__in=[
RequestGroup.TIME_CRITICAL, RequestGroup.RAPID_RESPONSE, RequestGroup.DIRECT],
start__lt=end,
end__gt=start,
site=site,
enclosure=enclosure,
telescope=telescope
)
if future_important_obs.count() > 0:
raise serializers.ValidationError(
_("This session overlaps a currently scheduled high priority observation. Please try again at a different time or resource"))


class SummarySerializer(serializers.ModelSerializer):
class Meta:
model = Summary
Expand Down Expand Up @@ -380,6 +428,16 @@ def validate(self, data):
if not interval_available:
raise serializers.ValidationError(_(f"The desired interval of {validated_data['start']} to {validated_data['end']} is not available on the telescope"))

# Now check a bunch of conditions on the real time interval to make sure its available
realtime_interval_availability_checks(
user,
validated_data['start'],
validated_data['end'],
validated_data['site'],
validated_data['enclosure'],
validated_data['telescope']
)

validated_data['proposal'] = proposal
# Add in the request group defaults for an observation
validated_data['observation_type'] = RequestGroup.REAL_TIME
Expand Down
Loading

0 comments on commit 74ed473

Please sign in to comment.