Skip to content

Commit

Permalink
Merge d473e7b into 9f355a7
Browse files Browse the repository at this point in the history
  • Loading branch information
jnation3406 committed Apr 11, 2022
2 parents 9f355a7 + d473e7b commit 21d3508
Show file tree
Hide file tree
Showing 8 changed files with 311 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.0.2 on 2022-04-05 23:26

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('observations', '0006_alter_summary_events'),
]

operations = [
migrations.AlterUniqueTogether(
name='configurationstatus',
unique_together=set(),
),
]
42 changes: 29 additions & 13 deletions observation_portal/observations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from django.utils.module_loading import import_string
from django.conf import settings
from datetime import timedelta
from collections import defaultdict
import copy

from observation_portal.requestgroups.models import Request, RequestGroup, Configuration, Location
import logging
Expand All @@ -26,18 +28,23 @@ def observation_as_dict(instance, no_request=False):
ret_dict['request_group_id'] = instance.request.request_group.id
ret_dict['created'] = instance.created
ret_dict['modified'] = instance.modified
configuration_status_by_config = {config_status.configuration.id: config_status
for config_status in instance.configuration_statuses.all()}
for configuration in ret_dict['request']['configurations']:
config_status = configuration_status_by_config[configuration['id']]
configuration['configuration_status'] = config_status.id
configuration['state'] = config_status.state
configuration['instrument_name'] = config_status.instrument_name
configuration['guide_camera_name'] = config_status.guide_camera_name
if hasattr(config_status, 'summary'):
configuration['summary'] = config_status.summary.as_dict()
else:
configuration['summary'] = {}
expanded_configurations = []
configuration_status_by_config = defaultdict(list)
for config_status in instance.configuration_statuses.all():
configuration_status_by_config[config_status.configuration.id].append(config_status)
for i in range(instance.request.configuration_repeats):
for configuration in ret_dict['request']['configurations']:
expanded_configurations.append(copy.deepcopy(configuration))
config_status = configuration_status_by_config[configuration['id']][i]
expanded_configurations[-1]['configuration_status'] = config_status.id
expanded_configurations[-1]['state'] = config_status.state
expanded_configurations[-1]['instrument_name'] = config_status.instrument_name
expanded_configurations[-1]['guide_camera_name'] = config_status.guide_camera_name
if hasattr(config_status, 'summary'):
expanded_configurations[-1]['summary'] = config_status.summary.as_dict()
else:
expanded_configurations[-1]['summary'] = {}
ret_dict['request']['configurations'] = expanded_configurations
return ret_dict


Expand Down Expand Up @@ -180,6 +187,16 @@ def delete_old_observations(cutoff):
def as_dict(self, no_request=False):
return import_string(settings.AS_DICT['observations']['Observation'])(self, no_request=no_request)

# Returns the current configuration repeat we are within the request for this configuration status
def get_current_repeat(self, configuration_status_id):
num_configurations = self.request.configurations.count()
configuration_status_index = 0
for cs in self.configuration_statuses.all():
if cs.id == configuration_status_id:
break
configuration_status_index += 1
return (configuration_status_index // num_configurations) + 1

@property
def instrument_types(self):
return set(self.request.configurations.values_list('instrument_type', flat=True))
Expand Down Expand Up @@ -227,7 +244,6 @@ def as_dict(self):
return import_string(settings.AS_DICT['observations']['ConfigurationStatus'])(self)

class Meta:
unique_together = ('configuration', 'observation')
verbose_name_plural = 'Configuration statuses'
ordering = ['id']

Expand Down
6 changes: 4 additions & 2 deletions observation_portal/observations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,16 @@ def update(self, instance, validated_data):
}
)

current_repeat = instance.observation.get_current_repeat(instance.id)

if 'end' in validated_data:
obs_end_time = validated_data['end']
obs_end_time += timedelta(seconds=instance.observation.request.get_remaining_duration(instance.configuration.priority))
obs_end_time += timedelta(seconds=instance.observation.request.get_remaining_duration(instance.configuration.priority, current_repeat=current_repeat))
instance.observation.update_end_time(obs_end_time)

if 'exposures_start_at' in validated_data:
obs_end_time = validated_data['exposures_start_at']
obs_end_time += timedelta(seconds=instance.observation.request.get_remaining_duration(instance.configuration.priority, include_current=True))
obs_end_time += timedelta(seconds=instance.observation.request.get_remaining_duration(instance.configuration.priority, include_current=True, current_repeat=current_repeat))
instance.observation.update_end_time(obs_end_time)

return instance
Expand Down
157 changes: 157 additions & 0 deletions observation_portal/observations/test/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,64 @@ def test_guide_camera_doesnt_match_science_camera_rejected(self):
self.assertEqual(response.status_code, 400)
self.assertIn('xx03 is not a valid guide camera for xx01', str(response.content))

def test_get_current_repeat_from_configuration_status_id(self):
requestgroup = self._generate_requestgroup()
request = requestgroup.requests.first()
create_simple_configuration(request, priority=request.configurations.first().priority + 1)
create_simple_configuration(request, priority=request.configurations.first().priority + 2)
request.configuration_repeats = 5
request.save()
configurations = list(request.configurations.all())

observation = self._generate_observation_data(
request.id,
[
configurations[0].id, configurations[1].id, configurations[2].id,
configurations[0].id, configurations[1].id, configurations[2].id,
configurations[0].id, configurations[1].id, configurations[2].id,
configurations[0].id, configurations[1].id, configurations[2].id,
configurations[0].id, configurations[1].id, configurations[2].id
]
)
self._create_observation(observation)
observation = Observation.objects.first()
configuration_statuses = observation.configuration_statuses.all()
self.assertEqual(observation.get_current_repeat(configuration_statuses[2].id), 1)
self.assertEqual(observation.get_current_repeat(configuration_statuses[3].id), 2)
self.assertEqual(observation.get_current_repeat(configuration_statuses[7].id), 3)
self.assertEqual(observation.get_current_repeat(configuration_statuses[10].id), 4)
self.assertEqual(observation.get_current_repeat(configuration_statuses[14].id), 5)

def test_get_all_configurations_from_schedule_endpoint_with_repeat_configurations(self):
requestgroup = self._generate_requestgroup()
request = requestgroup.requests.first()
create_simple_configuration(request, priority=request.configurations.first().priority + 1)
create_simple_configuration(request, priority=request.configurations.first().priority + 2)
request.configuration_repeats = 5
request.save()
configurations = list(request.configurations.all())
expected_configuration_ids = [
configurations[0].id, configurations[1].id, configurations[2].id,
configurations[0].id, configurations[1].id, configurations[2].id,
configurations[0].id, configurations[1].id, configurations[2].id,
configurations[0].id, configurations[1].id, configurations[2].id,
configurations[0].id, configurations[1].id, configurations[2].id
]
observation = self._generate_observation_data(
request.id,
expected_configuration_ids
)
self._create_observation(observation)
response = self.client.get(reverse('api:schedule-list'))
observation = response.json()['results'][0]

# Ensure configurations are repeated, and configuration statuses are ascending
previous_configuration_status_id = 0
for i, configuration in enumerate(observation['request']['configurations']):
self.assertEqual(expected_configuration_ids[i], configuration['id'])
self.assertGreater(configuration['configuration_status'], previous_configuration_status_id)
previous_configuration_status_id = configuration['configuration_status']


class TestUpdateConfigurationStatusApi(TestObservationApiBase):
def setUp(self):
Expand Down Expand Up @@ -1176,6 +1234,80 @@ def test_update_configuration_status_end_time_succeeds(self):
observation = Observation.objects.first()
self.assertEqual(observation.end, new_end)

def test_update_configuration_status_end_time_with_repeat_configurations_succeeds(self):
requestgroup = self._generate_requestgroup()
request = requestgroup.requests.first()
create_simple_configuration(request, priority=request.configurations.first().priority + 1)
request.configuration_repeats = 3
request.save()
configurations = list(request.configurations.all())
configuration_1_duration = configurations[0].duration
configuration_2_duration = configurations[1].duration

observation = self._generate_observation_data(
request.id,
[configurations[0].id, configurations[1].id, configurations[0].id, configurations[1].id, configurations[0].id, configurations[1].id]
)
self._create_observation(observation)
configuration_statuses = ConfigurationStatus.objects.all()
new_config_end = datetime(2016, 9, 5, 23, 47, 22).replace(tzinfo=timezone.utc)
update_data = {"end": datetime.strftime(new_config_end, '%Y-%m-%dT%H:%M:%SZ')}
self.client.patch(reverse('api:configurationstatus-detail', args=(configuration_statuses[0].id,)), update_data)
observation = Observation.objects.first()
slew_and_oe_switching_time = 10 # 3 * minimum slew of 2s + 2 * oe change time of 2s
new_observation_end = new_config_end + timedelta(seconds=(configuration_1_duration*2 + configuration_2_duration*3 + slew_and_oe_switching_time))
self.assertEqual(observation.end, new_observation_end)

def test_update_configuration_status_end_time_with_repeat_configurations_mid_repeat_succeeds(self):
requestgroup = self._generate_requestgroup()
request = requestgroup.requests.first()
create_simple_configuration(request, priority=request.configurations.first().priority + 1)
request.configuration_repeats = 3
request.save()
configurations = list(request.configurations.all())
configuration_1_duration = configurations[0].duration
configuration_2_duration = configurations[1].duration

observation = self._generate_observation_data(
request.id,
[configurations[0].id, configurations[1].id, configurations[0].id, configurations[1].id, configurations[0].id, configurations[1].id]
)
self._create_observation(observation)
configuration_statuses = ConfigurationStatus.objects.all()
new_config_end = datetime(2016, 9, 5, 23, 47, 22).replace(tzinfo=timezone.utc)
update_data = {"end": datetime.strftime(new_config_end, '%Y-%m-%dT%H:%M:%SZ')}
# Updating configuration status 2, so there should be 3 left to add to end time
self.client.patch(reverse('api:configurationstatus-detail', args=(configuration_statuses[2].id,)), update_data)
observation = Observation.objects.first()
slew_and_oe_switching_time = 6 # 2 * minimum slew of 2s + 1 * oe change time of 2s
new_observation_end = new_config_end + timedelta(seconds=(configuration_1_duration*1 + configuration_2_duration*2 + slew_and_oe_switching_time))
self.assertEqual(observation.end, new_observation_end)

def test_update_configuration_status_update_start_time_with_repeat_configurations_last_repeat_succeeds(self):
requestgroup = self._generate_requestgroup()
request = requestgroup.requests.first()
create_simple_configuration(request, priority=request.configurations.first().priority + 1)
request.configuration_repeats = 3
request.save()
configurations = list(request.configurations.all())
configuration_1_duration = configurations[0].duration
configuration_2_duration = configurations[1].duration

observation = self._generate_observation_data(
request.id,
[configurations[0].id, configurations[1].id, configurations[0].id, configurations[1].id, configurations[0].id, configurations[1].id]
)
self._create_observation(observation)
configuration_statuses = ConfigurationStatus.objects.all()
new_config_start = datetime(2016, 9, 5, 23, 47, 22).replace(tzinfo=timezone.utc)
update_data = {"exposures_start_at": datetime.strftime(new_config_start, '%Y-%m-%dT%H:%M:%SZ')}
# Updating configuration status 2, so there should be 3 left to add to end time
self.client.patch(reverse('api:configurationstatus-detail', args=(configuration_statuses[4].id,)), update_data)
observation = Observation.objects.first()
config_front_padding = 14 # not used for the current configuration 16s front padding - 2s oe change time
new_observation_end = new_config_start + timedelta(seconds=(configuration_1_duration*1 + configuration_2_duration*1 - config_front_padding))
self.assertEqual(observation.end, new_observation_end)

def test_update_configuration_status_exposure_start_time_succeeds(self):
observation = self._generate_observation_data(
self.requestgroup.requests.first().id, [self.requestgroup.requests.first().configurations.first().id]
Expand Down Expand Up @@ -1254,6 +1386,31 @@ def test_shorten_first_configuration_status_exposure_start_with_multiple_configs
configuration_status.configuration.priority, include_current=True))
self.assertEqual(observation.end, new_obs_end)

def test_shorten_first_configuration_status_exposure_start_with_repeat_configurations_multiple_configs(self):
requestgroup = self._generate_requestgroup()
request = requestgroup.requests.first()
create_simple_configuration(request, priority=request.configurations.first().priority + 1)
request.configuration_repeats = 3
request.save()
configurations = list(request.configurations.all())
configuration_1_duration = configurations[0].duration
configuration_2_duration = configurations[1].duration

observation = self._generate_observation_data(
request.id,
[configurations[0].id, configurations[1].id, configurations[0].id, configurations[1].id, configurations[0].id, configurations[1].id]
)
self._create_observation(observation)
configuration_statuses = ConfigurationStatus.objects.all()
new_config_start = datetime(2016, 9, 5, 22, 35, 45).replace(tzinfo=timezone.utc)
update_data = {"exposures_start_at": datetime.strftime(new_config_start, '%Y-%m-%dT%H:%M:%SZ')}
# Updating configuration status 1, so there should be 4 left to add to end time
self.client.patch(reverse('api:configurationstatus-detail', args=(configuration_statuses[1].id,)), update_data)
observation = Observation.objects.first()
config_front_padding = 8 # not used for the current configuration 16s front padding - 2*2s minimum slew and 2*2s oe change time
new_observation_end = new_config_start + timedelta(seconds=(configuration_1_duration*2 + configuration_2_duration*3 - config_front_padding))
self.assertEqual(observation.end, new_observation_end)

def test_configuration_status_exposure_start_cant_be_before_observation_start(self):
observation = self._generate_observation_data(self.requestgroup.requests.first().id,
[self.requestgroup.requests.first().configurations.first().id]
Expand Down
12 changes: 7 additions & 5 deletions observation_portal/requestgroups/duration_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,15 @@ def get_request_duration_by_instrument_type(request_dict):
durations_by_instrument_type = get_complete_configurations_duration_by_instrument_type(
configurations, start_time)

# Add in the front_padding proportionally by instrument_type here
# TODO: We should move front_padding to the telescope level rather than instrument_type so we don't need to
# assign it's value proportionally to observation time used per instrument_type
total_duration = 0.0
for duration in durations_by_instrument_type.values():
total_duration += duration
for instrument_type in durations_by_instrument_type.keys():
# First multiply the configuration durations by the configuration_repeats count
durations_by_instrument_type[instrument_type] *= request_dict.get('configuration_repeats', 1)
total_duration += durations_by_instrument_type[instrument_type]
for instrument_type in durations_by_instrument_type.keys():
# Then add in the front_padding proportionally by instrument_type here
# TODO: We should move front_padding to the telescope level rather than instrument_type so we don't need to
# assign it's value proportionally to observation time used per instrument_type
request_overheads = configdb.get_request_overheads(instrument_type)
durations_by_instrument_type[instrument_type] += (durations_by_instrument_type[instrument_type] / total_duration) * request_overheads['observation_front_padding']

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.0.2 on 2022-04-04 23:30

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('requestgroups', '0018_alter_acquisitionconfig_extra_params_and_more'),
]

operations = [
migrations.AddField(
model_name='request',
name='configuration_repeats',
field=models.PositiveIntegerField(default=1, help_text='The number of times to repeat the set of Configurations in this Request. This is normally used to nod between two or more separate Targets. This field must be set to a value greater than 0.', validators=[django.core.validators.MinValueValidator(1)]),
),
]
Loading

0 comments on commit 21d3508

Please sign in to comment.