def get(self, request):
        info = {}
        # Staff users by default should see all instruments, but can request only schedulable instruments.
        # Non-staff users are only allowed access to schedulable instruments.
        if request.user.is_staff:
            only_schedulable = request.query_params.get(
                'only_schedulable', False)
        else:
            only_schedulable = True

        requested_instrument_type = request.query_params.get(
            'instrument_type', '')
        location = {
            'site': request.query_params.get('site', ''),
            'enclosure': request.query_params.get('enclosure', ''),
            'telescope_class': request.query_params.get('telescope_class', ''),
            'telescope': request.query_params.get('telescope', ''),
        }
        for instrument_type in configdb.get_instrument_type_codes(
                location=location, only_schedulable=only_schedulable):
            if not requested_instrument_type or requested_instrument_type.lower(
            ) == instrument_type.lower():
                ccd_size = configdb.get_ccd_size(instrument_type)
                info[instrument_type] = {
                    'type':
                    configdb.get_instrument_type_category(instrument_type),
                    'class':
                    configdb.get_instrument_type_telescope_class(
                        instrument_type),
                    'name':
                    configdb.get_instrument_type_full_name(instrument_type),
                    'optical_elements':
                    configdb.get_optical_elements(instrument_type),
                    'modes':
                    configdb.get_modes_by_type(instrument_type),
                    'default_acceptability_threshold':
                    configdb.get_default_acceptability_threshold(
                        instrument_type),
                    'configuration_types':
                    configdb.get_configuration_types(instrument_type),
                    'default_configuration_type':
                    configdb.get_default_configuration_type(instrument_type),
                    'camera_type': {
                        'science_field_of_view':
                        configdb.get_diagonal_ccd_fov(instrument_type,
                                                      autoguider=False),
                        'autoguider_field_of_view':
                        configdb.get_diagonal_ccd_fov(instrument_type,
                                                      autoguider=True),
                        'pixel_scale':
                        configdb.get_pixel_scale(instrument_type),
                        'pixels_x':
                        ccd_size['x'],
                        'pixels_y':
                        ccd_size['y'],
                        'orientation':
                        configdb.get_average_ccd_orientation(instrument_type)
                    }
                }
        return Response(info)
Example #2
0
    def get(self, request):
        info = {}
        # Staff users by default should see all instruments, but can request only schedulable instruments.
        # Non-staff users are only allowed access to schedulable instruments.
        if request.user.is_staff:
            only_schedulable = request.query_params.get('only_schedulable', False)
        else:
            only_schedulable = True

        requested_instrument_type = request.query_params.get('instrument_type', '')
        location = {
            'site': request.query_params.get('site', ''),
            'enclosure': request.query_params.get('enclosure', ''),
            'telescope_class': request.query_params.get('telescope_class', ''),
            'telescope': request.query_params.get('telescope', ''),
        }
        for instrument_type in configdb.get_instrument_types(location=location, only_schedulable=only_schedulable):
            if not requested_instrument_type or requested_instrument_type.lower() == instrument_type.lower():
                info[instrument_type] = {
                    'type': 'SPECTRA' if configdb.is_spectrograph(instrument_type) else 'IMAGE',
                    'class': configdb.get_instrument_type_telescope_class(instrument_type),
                    'name': configdb.get_instrument_type_full_name(instrument_type),
                    'optical_elements': configdb.get_optical_elements(instrument_type),
                    'modes': configdb.get_modes_by_type(instrument_type),
                    'default_acceptability_threshold': configdb.get_default_acceptability_threshold(instrument_type)
                }
        return Response(info)
Example #3
0
    def test_validate_mode_config_filled_in_when_missing(self):
        guiding_config = {}
        modes = configdb.get_modes_by_type(self.request_instrument_type)
        validation_helper = ModeValidationHelper('guiding',
                                                 self.request_instrument_type,
                                                 modes['guiding'])

        validated_config = validation_helper.validate(guiding_config)
        self.assertEqual(validated_config['mode'], 'OFF')
Example #4
0
    def test_validate_mode_config_bad_config(self):
        instrument_config = self.instrument_config.copy()
        instrument_config['mode'] = "1m0_sbig_2"
        instrument_config['extra_params'] = {'bin_x': 1, 'bin_y': 2}
        modes = configdb.get_modes_by_type(self.request_instrument_type)

        validation_helper = ModeValidationHelper('readout',
                                                 self.request_instrument_type,
                                                 modes['readout'])
        with self.assertRaises(ValidationError) as e:
            validation_helper.validate(instrument_config)
        self.assertIn('bin_x', str(e.exception))
Example #5
0
    def test_validate_mode_config_good_config(self):
        instrument_config = self.instrument_config.copy()
        instrument_config['mode'] = "1m0_sbig_1"
        instrument_config['extra_params'] = {'bin_x': 1, 'bin_y': 1}
        modes = configdb.get_modes_by_type(self.request_instrument_type)

        validation_helper = ModeValidationHelper('readout',
                                                 self.request_instrument_type,
                                                 modes['readout'])
        validated_config = validation_helper.validate(instrument_config)

        self.assertEqual(validated_config, instrument_config)
Example #6
0
    def _instrument_defaults(self, instrument_type):
        '''Get a set of default values for optical elements and modes for an instrument'''
        modes = configdb.get_modes_by_type(instrument_type)
        optical_elements = configdb.get_optical_elements(instrument_type)
        defaults = {'optical_elements': {}}
        for oe_type, elements in optical_elements.items():
            for element in elements:
                if element['default']:
                    defaults['optical_elements'][
                        oe_type[:-1]] = element['code']
        for mode_type, mode_group in modes.items():
            defaults[mode_type] = mode_group.get('default', '')

        return defaults
Example #7
0
    def test_validate_extra_param_mode_good_config(self):
        instrument_config = self.instrument_config.copy()
        instrument_type = "2M0-SCICAM-MUSCAT"
        instrument_config['instrument_type'] = instrument_type
        instrument_config['extra_params'] = self.muscat_extra_params
        modes = configdb.get_modes_by_type(instrument_type)

        validation_helper = ModeValidationHelper('exposure',
                                                 instrument_type,
                                                 modes['exposure'],
                                                 is_extra_param_mode=True)
        validated_instrument_config = validation_helper.validate(
            instrument_config)

        self.assertEqual(instrument_config, validated_instrument_config)
Example #8
0
    def test_validate_extra_param_mode_bad_config(self):
        instrument_config = self.instrument_config.copy()
        instrument_type = "2M0-SCICAM-MUSCAT"
        instrument_config['instrument_type'] = instrument_type
        instrument_config['extra_params'] = self.muscat_extra_params.copy()
        del instrument_config['extra_params']['exposure_mode']
        modes = configdb.get_modes_by_type(instrument_type)

        validation_helper = ModeValidationHelper('exposure',
                                                 instrument_type,
                                                 modes['exposure'],
                                                 is_extra_param_mode=True)

        with self.assertRaises(ValidationError) as e:
            validation_helper.validate(instrument_config)
        self.assertIn('exposure_mode', str(e.exception))
Example #9
0
 def get(self, request):
     info = {}
     is_staff = request.user.is_staff
     for instrument_type in configdb.get_instrument_types(
         {}, only_schedulable=(not is_staff)):
         info[instrument_type] = {
             'type':
             'SPECTRA'
             if configdb.is_spectrograph(instrument_type) else 'IMAGE',
             'class':
             configdb.get_instrument_type_telescope_class(instrument_type),
             'name':
             configdb.get_instrument_type_full_name(instrument_type),
             'optical_elements':
             configdb.get_optical_elements(instrument_type),
             'modes':
             configdb.get_modes_by_type(instrument_type),
             'default_acceptability_threshold':
             configdb.get_default_acceptability_threshold(instrument_type)
         }
     return Response(info)
Example #10
0
    def validate(self, data):
        # TODO: Validate the guiding optical elements on the guiding instrument types
        instrument_type = data['instrument_type']
        modes = configdb.get_modes_by_type(instrument_type)
        default_modes = configdb.get_default_modes_by_type(instrument_type)
        guiding_config = data['guiding_config']

        if len(data['instrument_configs']) > 1 and data['type'] in ['SCRIPT', 'SKY_FLAT']:
            raise serializers.ValidationError(_(f'Multiple instrument configs are not allowed for type {data["type"]}'))

        # Validate the guide mode
        guide_validation_helper = ModeValidationHelper('guiding', instrument_type, default_modes, modes)
        if guide_validation_helper.mode_is_not_set(guiding_config):
            guide_mode_to_set = guide_validation_helper.get_mode_to_set()
            if guide_mode_to_set['error']:
                raise serializers.ValidationError(_(guide_mode_to_set['error']))
            if guide_mode_to_set['mode']:
                guiding_config['mode'] = guide_mode_to_set['mode']['code']
            else:
                guiding_config['mode'] = GuidingConfig.OFF

        guide_mode_error_msg = guide_validation_helper.get_mode_error_msg(guiding_config)
        if guide_mode_error_msg:
            raise serializers.ValidationError(_(guide_mode_error_msg))

        if configdb.is_spectrograph(instrument_type) and data['type'] not in ['LAMP_FLAT', 'ARC', 'NRES_BIAS', 'NRES_DARK']:
            if 'optional' in guiding_config and guiding_config['optional']:
                raise serializers.ValidationError(_(
                    "Guiding cannot be optional on spectrograph instruments for types that are not ARC or LAMP_FLAT."
                ))
            guiding_config['optional'] = False

        if data['type'] in ['LAMP_FLAT', 'ARC', 'AUTO_FOCUS', 'NRES_BIAS', 'NRES_DARK', 'BIAS', 'DARK', 'SCRIPT']:
            # These types of observations should only ever be set to guiding mode OFF, but the acquisition modes for
            # spectrographs won't necessarily have that mode. Force OFF here.
            data['acquisition_config']['mode'] = AcquisitionConfig.OFF
        else:
            # Validate acquire modes
            acquisition_config = data['acquisition_config']
            acquire_validation_helper = ModeValidationHelper('acquisition', instrument_type, default_modes, modes)
            if acquire_validation_helper.mode_is_not_set(acquisition_config):
                acquire_mode_to_set = acquire_validation_helper.get_mode_to_set()
                if acquire_mode_to_set['error']:
                    raise serializers.ValidationError(_(acquire_mode_to_set['error']))
                if acquire_mode_to_set['mode']:
                    acquisition_config['mode'] = acquire_mode_to_set['mode']['code']
                else:
                    acquisition_config['mode'] = AcquisitionConfig.OFF

            acquire_mode_error_msg = acquire_validation_helper.get_mode_error_msg(acquisition_config)
            if acquire_mode_error_msg:
                raise serializers.ValidationError(_(acquire_mode_error_msg))

        available_optical_elements = configdb.get_optical_elements(instrument_type)
        for instrument_config in data['instrument_configs']:
            # Validate the readout mode and the binning. Readout modes and binning are tied
            # together- If one is set, we can determine the other.
            # TODO: Remove the binning checks when binnings are removed entirely
            readout_validation_helper = ModeValidationHelper('readout', instrument_type, default_modes, modes)
            if readout_validation_helper.mode_is_not_set(instrument_config):
                if 'bin_x' not in instrument_config and 'bin_y' not in instrument_config:
                    # Set the readout mode as well as the binning
                    readout_mode_to_set = readout_validation_helper.get_mode_to_set()
                    if readout_mode_to_set['error']:
                        raise serializers.ValidationError(_(readout_mode_to_set['error']))
                    if readout_mode_to_set['mode']:
                        instrument_config['mode'] = readout_mode_to_set['mode']['code']
                        instrument_config['bin_x'] = readout_mode_to_set['mode']['params']['binning']
                        instrument_config['bin_y'] = readout_mode_to_set['mode']['params']['binning']

                elif 'bin_x' in instrument_config:
                    # A binning is set already - figure out what the readout mode should be from that
                    try:
                        instrument_config['mode'] = configdb.get_readout_mode_with_binning(
                            instrument_type, instrument_config['bin_x']
                        )['code']
                    except ConfigDBException as cdbe:
                        raise serializers.ValidationError(_(str(cdbe)))
            else:
                # A readout mode is set - validate the mode
                readout_error_msg = readout_validation_helper.get_mode_error_msg(instrument_config)
                if readout_error_msg:
                    raise serializers.ValidationError(_(readout_error_msg))

                # At this point the readout mode that is set is valid. Now either set the binnings, or make
                # sure that those that are set are ok
                readout_mode = configdb.get_mode_with_code(instrument_type, instrument_config['mode'], 'readout')
                if 'bin_x' not in instrument_config:
                    instrument_config['bin_x'] = readout_mode['params']['binning']
                    instrument_config['bin_y'] = readout_mode['params']['binning']

                elif instrument_config['bin_x'] != readout_mode['params']['binning']:
                    raise serializers.ValidationError(_(
                        f'Binning {instrument_config["bin_x"]} is not a valid binning for readout mode '
                        f'{instrument_config["mode"]} for instrument type {instrument_type}'
                    ))

            # Validate the rotator modes
            if 'rotator' in modes:
                rotator_mode_validation_helper = ModeValidationHelper('rotator', instrument_type, default_modes, modes)
                if rotator_mode_validation_helper.mode_is_not_set(instrument_config):
                    rotator_mode_to_set = rotator_mode_validation_helper.get_mode_to_set()
                    if rotator_mode_to_set['error']:
                        raise serializers.ValidationError(_(rotator_mode_to_set['error']))
                    if rotator_mode_to_set['mode']:
                        instrument_config['rotator_mode'] = rotator_mode_to_set['mode']['code']

                rotator_error_msg = rotator_mode_validation_helper.get_mode_error_msg(instrument_config)
                if rotator_error_msg:
                    raise serializers.ValidationError(_(rotator_error_msg))

            # Check that the optical elements specified are valid in configdb
            for oe_type, value in instrument_config.get('optical_elements', {}).items():
                plural_type = '{}s'.format(oe_type)
                if plural_type not in available_optical_elements:
                    raise serializers.ValidationError(_("optical_element of type {} is not available on {} instruments"
                                                        .format(oe_type, data['instrument_type'])))
                available_elements = {element['code'].lower(): element['code'] for element in available_optical_elements[plural_type]}
                if plural_type in available_optical_elements and value.lower() not in available_elements.keys():
                    raise serializers.ValidationError(_("optical element {} of type {} is not available".format(
                        value, oe_type
                    )))
                else:
                    instrument_config['optical_elements'][oe_type] = available_elements[value.lower()]

            # Also check that any optical element group in configdb is specified in the request unless we are a BIAS or
            # DARK or SCRIPT type observation
            observation_types_without_oe = ['BIAS', 'DARK', 'SCRIPT']
            if data['type'].upper() not in observation_types_without_oe:
                for oe_type in available_optical_elements.keys():
                    singular_type = oe_type[:-1] if oe_type.endswith('s') else oe_type
                    if singular_type not in instrument_config.get('optical_elements', {}):
                        raise serializers.ValidationError(_(
                            f'Must set optical element of type {singular_type} for instrument type {instrument_type}'
                        ))
            # Validate any regions of interest
            if 'rois' in instrument_config:
                max_rois = configdb.get_max_rois(instrument_type)
                ccd_size = configdb.get_ccd_size(instrument_type)
                if len(instrument_config['rois']) > max_rois:
                    raise serializers.ValidationError(_(
                        f'Instrument type {instrument_type} supports up to {max_rois} regions of interest'
                    ))
                for roi in instrument_config['rois']:
                    if 'x1' not in roi and 'x2' not in roi and 'y1' not in roi and 'y2' not in roi:
                        raise serializers.ValidationError(_('Must submit at least one bound for a region of interest'))

                    if 'x1' not in roi:
                        roi['x1'] = 0
                    if 'x2' not in roi:
                        roi['x2'] = ccd_size['x']
                    if 'y1' not in roi:
                        roi['y1'] = 0
                    if 'y2' not in roi:
                        roi['y2'] = ccd_size['y']

                    if roi['x1'] >= roi['x2'] or roi['y1'] >= roi['y2']:
                        raise serializers.ValidationError(_(
                            'Region of interest pixels start must be less than pixels end'
                        ))

                    if roi['x2'] > ccd_size['x'] or roi['y2'] > ccd_size['y']:
                        raise serializers.ValidationError(_(
                            'Regions of interest for instrument type {} must be in range 0<=x<={} and 0<=y<={}'.format(
                                instrument_type, ccd_size['x'], ccd_size['y']
                            ))
                        )

        if data['type'] == 'SCRIPT':
            if (
                    'extra_params' not in data
                    or 'script_name' not in data['extra_params']
                    or not data['extra_params']['script_name']
            ):
                raise serializers.ValidationError(_(
                    'Must specify a script_name in extra_params for SCRIPT configuration type'
                ))

        # Validate duration is set if it's a REPEAT_* type configuration
        if 'REPEAT' in data['type']:
            if 'repeat_duration' not in data or data['repeat_duration'] is None:
                raise serializers.ValidationError(_(
                    f'Must specify a configuration repeat_duration for {data["type"]} type configurations.'
                ))
            else:
                # Validate that the duration exceeds the minimum to run everything at least once
                min_duration = sum(
                    [get_instrument_configuration_duration(
                        ic, data['instrument_type']) for ic in data['instrument_configs']]
                )
                if min_duration > data['repeat_duration']:
                    raise serializers.ValidationError(_(
                        f'Configuration repeat_duration of {data["repeat_duration"]} is less than the minimum of '
                        f'{min_duration} required to repeat at least once'
                    ))
        else:
            if 'repeat_duration' in data and data['repeat_duration'] is not None:
                raise serializers.ValidationError(_(
                    'You may only specify a repeat_duration for REPEAT_* type configurations.'
                ))

        # Validate the configuration type is available for the instrument requested
        if data['type'] not in configdb.get_configuration_types(instrument_type):
            raise serializers.ValidationError(_(
                f'configuration type {data["type"]} is not valid for instrument type {instrument_type}'
            ))
        return data
Example #11
0
    def validate(self, data):
        # TODO: Validate the guiding optical elements on the guiding instrument types
        instrument_type = data['instrument_type']
        modes = configdb.get_modes_by_type(instrument_type)
        guiding_config = data['guiding_config']

        if len(data['instrument_configs']) > 1 and data['type'] in [
                'SCRIPT', 'SKY_FLAT'
        ]:
            raise serializers.ValidationError(
                _(f'Multiple instrument configs are not allowed for type {data["type"]}'
                  ))

        # Validate the guide mode
        guide_validation_helper = ModeValidationHelper('guiding',
                                                       instrument_type,
                                                       modes['guiding'])

        guiding_config = guide_validation_helper.validate(guiding_config)
        if not guiding_config.get('mode'):
            # Guiding modes have an implicit default of OFF (we could just put this in all relevent validation_schema)
            guiding_config['mode'] = GuidingConfig.OFF
        data['guiding_config'] = guiding_config

        if configdb.is_spectrograph(instrument_type) and data['type'] not in [
                'LAMP_FLAT', 'ARC', 'NRES_BIAS', 'NRES_DARK'
        ]:
            if 'optional' in guiding_config and guiding_config['optional']:
                raise serializers.ValidationError(
                    _("Guiding cannot be optional on spectrograph instruments for types that are not ARC or LAMP_FLAT."
                      ))
            guiding_config['optional'] = False

        if data['type'] in [
                'LAMP_FLAT', 'ARC', 'AUTO_FOCUS', 'NRES_BIAS', 'NRES_DARK',
                'BIAS', 'DARK', 'SCRIPT'
        ]:
            # These types of observations should only ever be set to guiding mode OFF, but the acquisition modes for
            # spectrographs won't necessarily have that mode. Force OFF here.
            data['acquisition_config']['mode'] = AcquisitionConfig.OFF
        else:
            # Validate acquire modes
            acquisition_config = data['acquisition_config']
            acquire_validation_helper = ModeValidationHelper(
                'acquisition', instrument_type, modes['acquisition'])
            acquisition_config = acquire_validation_helper.validate(
                acquisition_config)
            if not acquisition_config.get('mode'):
                # Acquisition modes have an implicit default of OFF (we could just put this in all relevent validation_schema)
                acquisition_config['mode'] = AcquisitionConfig.OFF
            data['acquisition_config'] = acquisition_config

        available_optical_elements = configdb.get_optical_elements(
            instrument_type)
        for i, instrument_config in enumerate(data['instrument_configs']):
            # Validate the readout mode and the binning. Readout modes and binning are tied
            # together- If one is set, we can determine the other.
            # TODO: Remove the binning checks when binnings are removed entirely
            readout_mode = instrument_config.get('mode', '')
            if not readout_mode and 'bin_x' in instrument_config:
                # A binning is set already - figure out what the readout mode should be from that
                try:
                    readout_mode = instrument_config[
                        'mode'] = configdb.get_readout_mode_with_binning(
                            instrument_type,
                            instrument_config['bin_x'])['code']
                except ConfigDBException as cdbe:
                    raise serializers.ValidationError(_(str(cdbe)))
            readout_validation_helper = ModeValidationHelper(
                'readout', instrument_type, modes['readout'])
            instrument_config = readout_validation_helper.validate(
                instrument_config)
            data['instrument_configs'][i] = instrument_config

            # Validate the rotator modes
            if 'rotator' in modes:
                rotator_mode_validation_helper = ModeValidationHelper(
                    'rotator',
                    instrument_type,
                    modes['rotator'],
                    mode_key='rotator_mode')
                instrument_config = rotator_mode_validation_helper.validate(
                    instrument_config)
                data['instrument_configs'][i] = instrument_config

            # Check that the optical elements specified are valid in configdb
            for oe_type, value in instrument_config.get(
                    'optical_elements', {}).items():
                plural_type = '{}s'.format(oe_type)
                if plural_type not in available_optical_elements:
                    raise serializers.ValidationError(
                        _("optical_element of type {} is not available on {} instruments"
                          .format(oe_type, data['instrument_type'])))
                available_elements = {
                    element['code'].lower(): element['code']
                    for element in available_optical_elements[plural_type]
                }
                if plural_type in available_optical_elements and value.lower(
                ) not in available_elements.keys():
                    raise serializers.ValidationError(
                        _("optical element {} of type {} is not available".
                          format(value, oe_type)))
                else:
                    instrument_config['optical_elements'][
                        oe_type] = available_elements[value.lower()]

            # Also check that any optical element group in configdb is specified in the request unless we are a BIAS or
            # DARK or SCRIPT type observation
            observation_types_without_oe = ['BIAS', 'DARK', 'SCRIPT']
            if data['type'].upper() not in observation_types_without_oe:
                for oe_type in available_optical_elements.keys():
                    singular_type = oe_type[:-1] if oe_type.endswith(
                        's') else oe_type
                    if singular_type not in instrument_config.get(
                            'optical_elements', {}):
                        raise serializers.ValidationError(
                            _(f'Must set optical element of type {singular_type} for instrument type {instrument_type}'
                              ))
            # Validate any regions of interest
            if 'rois' in instrument_config:
                max_rois = configdb.get_max_rois(instrument_type)
                ccd_size = configdb.get_ccd_size(instrument_type)
                if len(instrument_config['rois']) > max_rois:
                    raise serializers.ValidationError(
                        _(f'Instrument type {instrument_type} supports up to {max_rois} regions of interest'
                          ))
                for roi in instrument_config['rois']:
                    if 'x1' not in roi and 'x2' not in roi and 'y1' not in roi and 'y2' not in roi:
                        raise serializers.ValidationError(
                            _('Must submit at least one bound for a region of interest'
                              ))

                    if 'x1' not in roi:
                        roi['x1'] = 0
                    if 'x2' not in roi:
                        roi['x2'] = ccd_size['x']
                    if 'y1' not in roi:
                        roi['y1'] = 0
                    if 'y2' not in roi:
                        roi['y2'] = ccd_size['y']

                    if roi['x1'] >= roi['x2'] or roi['y1'] >= roi['y2']:
                        raise serializers.ValidationError(
                            _('Region of interest pixels start must be less than pixels end'
                              ))

                    if roi['x2'] > ccd_size['x'] or roi['y2'] > ccd_size['y']:
                        raise serializers.ValidationError(
                            _('Regions of interest for instrument type {} must be in range 0<=x<={} and 0<=y<={}'
                              .format(instrument_type, ccd_size['x'],
                                      ccd_size['y'])))

            # Validate the exposure modes
            if 'exposure' in modes:
                exposure_mode_validation_helper = ModeValidationHelper(
                    'exposure',
                    instrument_type,
                    modes['exposure'],
                    mode_key='exposure_mode',
                    is_extra_param_mode=True)
                instrument_config = exposure_mode_validation_helper.validate(
                    instrument_config)
                data['instrument_configs'][i] = instrument_config

            # This applies the exposure_time requirement only to non-muscat instruments.
            # I wish I could figure out a better way to do this, but it needs info about the instrument_type which is a level up.
            if instrument_type.upper() != '2M0-SCICAM-MUSCAT':
                if instrument_config.get('exposure_time', None) is None:
                    raise serializers.ValidationError({
                        'instrument_configs': [{
                            'exposure_time': [_('This value cannot be null.')]
                        }]
                    })
                if isnan(instrument_config.get('exposure_time')):
                    raise serializers.ValidationError({
                        'instrument_configs': [{
                            'exposure_time':
                            [_('A valid number is required.')]
                        }]
                    })
                if instrument_config['exposure_time'] < 0:
                    raise serializers.ValidationError({
                        'instrument_configs': [{
                            'exposure_time':
                            [_('This value cannot be negative.')]
                        }]
                    })

        if data['type'] == 'SCRIPT':
            if ('extra_params' not in data
                    or 'script_name' not in data['extra_params']
                    or not data['extra_params']['script_name']):
                raise serializers.ValidationError(
                    _('Must specify a script_name in extra_params for SCRIPT configuration type'
                      ))

        # Validate duration is set if it's a REPEAT_* type configuration
        if 'REPEAT' in data['type']:
            if 'repeat_duration' not in data or data['repeat_duration'] is None:
                raise serializers.ValidationError(
                    _(f'Must specify a configuration repeat_duration for {data["type"]} type configurations.'
                      ))
            else:
                # Validate that the duration exceeds the minimum to run everything at least once
                min_duration = sum([
                    get_instrument_configuration_duration(
                        ic, data['instrument_type'])
                    for ic in data['instrument_configs']
                ])
                if min_duration > data['repeat_duration']:
                    raise serializers.ValidationError(
                        _(f'Configuration repeat_duration of {data["repeat_duration"]} is less than the minimum of '
                          f'{min_duration} required to repeat at least once'))
        else:
            if 'repeat_duration' in data and data[
                    'repeat_duration'] is not None:
                raise serializers.ValidationError(
                    _('You may only specify a repeat_duration for REPEAT_* type configurations.'
                      ))

        # Validate the configuration type is available for the instrument requested
        if data['type'] not in configdb.get_configuration_types(
                instrument_type):
            raise serializers.ValidationError(
                _(f'configuration type {data["type"]} is not valid for instrument type {instrument_type}'
                  ))

        return data