def _test_calculation(self, rms, int_, elapsed, t_rx, t_sys, *args, **kwargs): """ The "tol_factor" kwarg can be used to ease the tolerance by a given factor for the "integration time" calculated for elapsed time and the elapsed time calculated from "integration time". This is useful in the case of basket-weaved rasters. """ tol_factor = kwargs.get('tol_factor', 1.0) itc = HeterodyneITC() (result, extra) = itc.calculate_time( rms, *args, with_extra_output=True) self.assertAlmostEqual(extra['t_rx'], t_rx, delta=0.1) self.assertAlmostEqual(extra['t_sys'], t_sys, delta=0.1) self.assertAlmostEqual(extra['int_time'], int_, delta=0.01) self.assertAlmostEqual(result, elapsed, delta=0.1) (result, extra) = itc.calculate_rms_for_elapsed_time( elapsed, *args, with_extra_output=True) self.assertAlmostEqual(extra['t_rx'], t_rx, delta=0.1) self.assertAlmostEqual(extra['t_sys'], t_sys, delta=0.1) self.assertAlmostEqual(extra['int_time'], int_, delta=(0.01 * tol_factor)) self.assertAlmostEqual(result, rms, delta=0.1) (result, extra) = itc.calculate_rms_for_int_time( int_, *args, with_extra_output=True) self.assertAlmostEqual(extra['t_rx'], t_rx, delta=0.1) self.assertAlmostEqual(extra['t_sys'], t_sys, delta=0.1) self.assertAlmostEqual(result, rms, delta=0.1) self.assertAlmostEqual(extra['elapsed_time'], elapsed, delta=(5 * tol_factor))
def __init__(self, *args): """ Construct calculator. Calls the superclass constructor and then initializes a Heterodyne ITC object. """ super(HeterodyneCalculator, self).__init__(*args) self.itc = HeterodyneITC() # Determine which combinations of mapping and switching modes # are allowed. valid_modes = self.itc.get_valid_modes() self.map_modes = OrderedDict( ((map_code, map_mode._replace(sw_modes=[ sw_code for (sw_code, sw_mode) in self.switch_modes.items() if (map_mode.id, sw_mode.id) in valid_modes ])) for (map_code, map_mode) in self._map_modes.items()))
def test_int_time_limit(self): itc = HeterodyneITC() args = [ HeterodyneReceiver.HARP, HeterodyneITC.RASTER, HeterodyneITC.PSSW, 330, 0.977, 0.04, 75, False, False, None, 300, 400, 7.27, 7.3, False, False, False, ] with self.assertRaisesRegexp( HeterodyneITCError, '^The requested integration time per point is less than'): itc.calculate_rms_for_int_time(0.09, *args) with self.assertRaisesRegexp( HeterodyneITCError, '^The requested target sensitivity led to an integration t'): itc.calculate_time(1.0, *args) with self.assertRaisesRegexp( HeterodyneITCError, '^The requested elapsed time led to an integration t'): itc.calculate_rms_for_elapsed_time(500, *args)
class HeterodyneCalculator(JCMTCalculator): CALC_TIME = 1 CALC_RMS_FROM_ELAPSED_TIME = 2 CALC_RMS_FROM_INT_TIME = 3 modes = OrderedDict(( (CALC_TIME, CalculatorMode('time', 'Time required for target RMS')), (CALC_RMS_FROM_ELAPSED_TIME, CalculatorMode('rms_el', 'RMS expected in elapsed time')), (CALC_RMS_FROM_INT_TIME, CalculatorMode('rms_int', 'RMS for integration time per point')), )) _map_modes = OrderedDict(( ('grid', MappingMode(HeterodyneITC.GRID, 'Grid', None)), ('jiggle', MappingMode(HeterodyneITC.JIGGLE, 'Jiggle', None)), ('raster', MappingMode(HeterodyneITC.RASTER, 'Raster', None)), )) switch_modes = OrderedDict(( ('bmsw', SwitchingMode(HeterodyneITC.BMSW, 'Beam')), ('pssw', SwitchingMode(HeterodyneITC.PSSW, 'Position')), ('frsw', SwitchingMode(HeterodyneITC.FRSW, 'Frequency')), )) # Note: the JavaScript assumes that each array-only mode is preceeded # by the equivalent non-array-only mode. In other words, if an array-only # mode is selected when a non-array receiver is chosen, it should change # to the preceeding mode. acsis_modes = OrderedDict(( (1, ACSISMode('250 MHz', 0.0305, False)), (2, ACSISMode('400 MHz', 0.061, True)), (3, ACSISMode('1000 MHz', 0.488, False)), (4, ACSISMode('1600 MHz', 0.977, True)), )) version = 1 @classmethod def get_code(cls): """ Get the calculator "code". This is a short string used to uniquely identify the calculator within the facility which uses it. """ return 'heterodyne' def __init__(self, *args): """ Construct calculator. Calls the superclass constructor and then initializes a Heterodyne ITC object. """ super(HeterodyneCalculator, self).__init__(*args) self.itc = HeterodyneITC() # Determine which combinations of mapping and switching modes # are allowed. valid_modes = self.itc.get_valid_modes() self.map_modes = OrderedDict( ((map_code, map_mode._replace(sw_modes=[ sw_code for (sw_code, sw_mode) in self.switch_modes.items() if (map_mode.id, sw_mode.id) in valid_modes ])) for (map_code, map_mode) in self._map_modes.items())) def get_name(self): return 'Heterodyne ITC' def get_calc_version(self): return self.itc.get_version() def get_inputs(self, mode, version=None): """ Get the list of calculator inputs for a given version of the calculator. """ if version is None: version = self.version inputs = SectionedList() if version == 1: inputs.extend([ CalculatorValue('rx', 'Receiver', 'Receiver', '{}', None), CalculatorValue('freq', 'Frequency', '\u03bd', '{:.3f}', 'GHz'), CalculatorValue('res', 'Spectral resolution', '\u0394\u03bd', '{:.4f}', None), CalculatorValue('res_unit', 'Resolution unit', '\u0394\u03bd unit', '{}', None), CalculatorValue('sb', 'Sideband mode', 'SB', '{}', None), CalculatorValue('dual_pol', 'Dual polarization', 'DP', '{}', None), CalculatorValue('cont', 'Continuum mode', 'CM', '{}', None), ], section='rx', section_name='Receiver') inputs.extend([ CalculatorValue('pos', 'Source position', 'Pos.', '{:.1f}', '\u00b0'), CalculatorValue('pos_type', 'Source position type', 'Pos. type', '{}', None), CalculatorValue('tau', '225 GHz opacity', '\u03c4\u2082\u2082\u2085', '{:.3f}', None), ], section='src', section_name='Source and Conditions') inputs.extend([ CalculatorValue('mm', 'Mapping mode', 'Mode', '{}', None), CalculatorValue('sw', 'Switching mode', 'Switch', '{}', None), CalculatorValue('n_pt', 'Number of points', 'Points', '{:d}', None), CalculatorValue('sep_off', 'Separate offs', 'SO', '{}', None), CalculatorValue('dim_x', 'Raster width', 'x', '{}', '"'), CalculatorValue('dim_y', 'Raster height', 'y', '{}', '"'), CalculatorValue('dx', 'Pixel width', 'dx', '{}', '"'), CalculatorValue('dy', 'Pixel/scan height', 'dy', '{}', '"'), CalculatorValue('basket', 'Basket weave', 'BW', '{}', None), ], section='obs', section_name='Observation') else: raise CalculatorError('Unknown version.') inputs.add_section('req', 'Requirement') if mode == self.CALC_TIME: if version == 1: inputs.extend([ CalculatorValue('rms', 'Target sensitivity', '\u03c3', '{:.3f}', 'K TA*'), ], section='req') else: raise CalculatorError('Unknown version.') elif mode == self.CALC_RMS_FROM_ELAPSED_TIME: if version == 1: inputs.extend([ CalculatorValue('elapsed', 'Elapsed time', 'Elapsed', '{:.3f}', 'hours'), ], section='req') else: raise CalculatorError('Unknown version.') elif mode == self.CALC_RMS_FROM_INT_TIME: if version == 1: inputs.extend([ CalculatorValue('int_time', 'Integration time', 'Int. time', '{:.3f}', 'seconds'), ], section='req') else: raise CalculatorError('Unknown version.') else: raise CalculatorError('Unknown mode.') return inputs def get_default_input(self, mode): """ Get the default input values (for the current version). """ common_inputs = [ ('rx', 'HARP'), ('mm', 'raster'), ('sw', 'pssw'), ('freq', 345.796), ('res', 0.488), ('res_unit', 'MHz'), ('tau', 0.1), ('pos', 40.0), ('pos_type', 'dec'), ('sb', 'ssb'), ('dual_pol', False), ('n_pt', 1), ('dim_x', 180), ('dim_y', 180), ('dx', 8), ('dy', 8), ('basket', False), ('sep_off', False), ('cont', False), ] if mode == self.CALC_TIME: return dict(common_inputs + [ ('rms', 1.0), ]) elif mode == self.CALC_RMS_FROM_ELAPSED_TIME: return dict(common_inputs + [ ('elapsed', 0.75), ]) elif mode == self.CALC_RMS_FROM_INT_TIME: return dict(common_inputs + [ ('int_time', 30.0), ]) else: raise CalculatorError('Unknown mode.') def format_input(self, inputs, values): """ Format input values for display in the input form. """ defaults = self.get_default_input(self.CALC_TIME) is_array_receiver = self.get_receiver_by_name( values['rx'], as_object=True).array is not None formatted_inputs = { x.code: x.format.format(values[x.code] if values[x.code] is not None else defaults.get(x.code)) if x.code not in ('rx', 'mm', 'sw', 'sb', 'dual_pol', 'n_pt', 'basket', 'sep_off', 'cont', 'res_unit') else (values[x.code] if values[x.code] is not None else defaults.get(x.code)) for x in inputs } acsis_mode = None if values['res_unit'] == 'MHz': for (acsis_mode_num, acsis_mode_info) in self.acsis_modes.items(): if acsis_mode_info.array_only and not is_array_receiver: continue if abs(values['res'] - acsis_mode_info.freq_res) < 0.0001: acsis_mode = acsis_mode_num break formatted_inputs.update({ 'tau_band': self.get_tau_band(values['tau']), 'acsis_mode': acsis_mode, 'dy_spacing': '{:.3f}'.format( values['dy'] if values['dy'] is not None else defaults['dy']), }) return formatted_inputs def get_form_input(self, inputs, form): """ Extract the input values from the submitted form. """ defaults = self.get_default_input(self.CALC_TIME) receiver = self.get_receiver_by_name(form['rx'], as_object=True) map_mode = self.map_modes[form['mm']].id values = {} for input_ in inputs: if input_.code in ('dual_pol', 'basket', 'sep_off', 'cont'): # Checkboxes: true if they appear in the form parameters. values[input_.code] = input_.code in form elif input_.code == 'tau': (tau, tau_band) = self.get_form_tau(form) values['tau'] = tau values['tau_band'] = tau_band elif input_.code == 'res': if form['acsis_mode'] == 'other': values[input_.code] = form[input_.code] values['acsis_mode'] = None else: acsis_mode = int(form['acsis_mode']) values[input_.code] = self.acsis_modes[acsis_mode].freq_res values['acsis_mode'] = acsis_mode elif input_.code == 'res_unit': if form['acsis_mode'] == 'other': values[input_.code] = form[input_.code] else: values[input_.code] = 'MHz' elif input_.code == 'n_pt': if map_mode == HeterodyneITC.GRID: try: values[input_.code] = int(form['n_pt']) except ValueError: # Prefer to convert to integer so that the value could # match a possible jiggle pattern point count, but if # that fails, leave as a string so that we can warn # as normal for a malformed value. values[input_.code] = form['n_pt'] elif map_mode == HeterodyneITC.JIGGLE: if receiver.array is None: values[input_.code] = int(form['n_pt_jiggle']) else: values[input_.code] = int(form['n_pt_jiggle_' + receiver.name]) else: values[input_.code] = defaults.get(input_.code, None) else: # Need to allow parameters not to exist when they can # be disabled (e.g. raster parameters in other modes). # For now, allow all input values to be filled from the # general defaults. TODO: be more specific here? value = form.get(input_.code, None) if value is None: value = defaults.get(input_.code, None) values[input_.code] = value values['dy_spacing'] = form.get('dy_spacing_' + receiver.name, None) if values['dy_spacing'] is None: values['dy_spacing'] = '{:.3f}'.format(defaults['dy']) return values def convert_input_mode(self, mode, new_mode, input_): """ Convert the inputs for one mode to form a suitable set of inputs for another mode. Only called if the mode is changed. """ new_input = input_.copy() result = self(mode, input_) if new_mode == self.CALC_TIME: if mode == self.CALC_RMS_FROM_ELAPSED_TIME: new_input['rms'] = result.output['rms'] del new_input['elapsed'] elif mode == self.CALC_RMS_FROM_INT_TIME: new_input['rms'] = result.output['rms'] del new_input['int_time'] else: raise CalculatorError('Impossible mode change.') elif new_mode == self.CALC_RMS_FROM_ELAPSED_TIME: if mode == self.CALC_TIME: new_input['elapsed'] = result.output['elapsed'] del new_input['rms'] elif mode == self.CALC_RMS_FROM_INT_TIME: new_input['elapsed'] = result.output['elapsed'] del new_input['int_time'] else: raise CalculatorError('Impossible mode change.') elif new_mode == self.CALC_RMS_FROM_INT_TIME: if mode == self.CALC_TIME: new_input['int_time'] = result.extra['int_time'] del new_input['rms'] elif mode == self.CALC_RMS_FROM_ELAPSED_TIME: new_input['int_time'] = result.extra['int_time'] del new_input['elapsed'] else: raise CalculatorError('Impossible mode change.') else: raise CalculatorError('Unknown mode.') return new_input def convert_input_version(self, mode, old_version, input_): """ Converts the inputs from an older version so that they can be used with the current version of the calculator. """ return input_ def get_outputs(self, mode, version=None): """ Get the list of calculator outputs for a given version of the calculator. """ if version is None: version = self.version if mode == self.CALC_TIME: if version == 1: return [ CalculatorValue('elapsed', 'Elapsed time', 'Elapsed', '{:.3f}', 'hours'), ] else: raise CalculatorError('Unknown version.') elif mode == self.CALC_RMS_FROM_ELAPSED_TIME: if version == 1: return [ CalculatorValue('rms', 'Sensitivity', '\u03c3', '{:.3f}', 'K TA*'), ] else: raise CalculatorError('Unknown version.') elif mode == self.CALC_RMS_FROM_INT_TIME: if version == 1: return [ CalculatorValue('rms', 'Sensitivity', '\u03c3', '{:.3f}', 'K TA*'), CalculatorValue('elapsed', 'Elapsed time', 'Elapsed', '{:.3f}', 'hours'), ] else: raise CalculatorError('Unknown version.') else: raise CalculatorError('Unknown mode.') def get_extra_context(self): """ Return extra information to be given to the view template. """ return { 'weather_bands': JCMTWeather.get_available(), 'receivers': HeterodyneReceiver.get_all_receivers().values(), 'map_modes': self.map_modes, 'switch_modes': self.switch_modes, 'jiggle_patterns': self.itc.get_jiggle_patterns(), 'acsis_modes': self.acsis_modes, 'int_time_minimum': self.itc.int_time_minimum, 'position_types': self.position_type, } def parse_input(self, mode, input_, defaults=None): """ Parse inputs as obtained from the HTML form (typically unicode) and return values suitable for calculation (perhaps float). """ receiver = self.get_receiver_by_name(input_['rx'], as_object=True) parsed = {} for field in self.get_inputs(mode): try: if field.code in ('freq', 'res', 'pos', 'rms', 'tau'): parsed[field.code] = float(input_[field.code]) elif field.code == 'dx': if receiver.array is None: parsed[field.code] = float(input_[field.code]) else: # The "dx" input is disabled for array receivers: # always use the pixel size. parsed[field.code] = receiver.pixel_size elif field.code == 'dy': if receiver.array is None: parsed[field.code] = float(input_[field.code]) else: parsed[field.code] = float(input_['dy_spacing']) elif field.code in ('dim_x', 'dim_y', 'int_time'): parsed[field.code] = float(input_[field.code]) elif field.code in ('n_pt'): parsed[field.code] = int(input_[field.code]) elif field.code == 'elapsed': parsed[field.code] = parse_time(input_[field.code]) else: parsed[field.code] = input_[field.code] except ValueError: if (not input_[field.code]) and (defaults is not None): parsed[field.code] = defaults[field.code] else: raise UserError('Invalid value for {}.', field.name) self._validate_position(parsed['pos'], parsed['pos_type']) map_mode = self.map_modes[parsed['mm']].id # Remove irrelevant input values for the given mode. if map_mode == HeterodyneITC.RASTER: parsed['n_pt'] = None else: parsed['dim_x'] = None parsed['dim_y'] = None parsed['dx'] = None parsed['dy'] = None return parsed def get_receiver_by_name(self, receiver_name, as_object=False): """ Get a receiver by name. """ for (rx_id, rx_info) in HeterodyneReceiver.get_all_receivers().items(): if rx_info.name == receiver_name: if as_object: return ReceiverInfoID(*rx_info, id=rx_id) return rx_id raise UserError('Receiver not recognised.') def __call__(self, mode, input_): """ Perform a calculation, taking an input dictionary and returning a CalculatorResult object. The result object contains the essential output, a dictionary with entries corresponding to the list given by get_outputs. It also contains any extra output for display but which would not be stored in the database as part of the calculation result. """ extra_output = {} if input_['pos_type'] == 'dec': zenith_angle_deg = self.itc.estimate_zenith_angle_deg( input_['pos']) elif input_['pos_type'] == 'zen': zenith_angle_deg = input_['pos'] elif input_['pos_type'] == 'el': zenith_angle_deg = 90.0 - input_['pos'] elif input_['pos_type'] == 'am': zenith_angle_deg = degrees(acos(1.0 / input_['pos'])) else: raise UserError('Unknown source position type.') if input_['pos_type'] != 'zen': extra_output['zenith_angle'] = zenith_angle_deg receiver = self.get_receiver_by_name(input_['rx'], as_object=True) freq = input_['freq'] freq_res = input_['res'] freq_res_unit = input_['res_unit'] if freq_res_unit == 'MHz': extra_output['res_velocity'] = \ self.itc.freq_res_to_velocity(freq, freq_res) elif freq_res_unit == 'km/s': freq_res = self.itc.velocity_to_freq_res(freq, freq_res) extra_output['res_freq'] = freq_res else: raise CalculatorError('Frequency units not recognised.') if freq < receiver.f_min: raise UserError( 'The frequency {} GHz is below the minimum ' 'frequency ({} GHz) of this receiver.', freq, receiver.f_min) elif freq > receiver.f_max: raise UserError( 'The frequency {} GHz is above the maximum ' 'frequency ({} GHz) of this receiver.', freq, receiver.f_max) kwargs = { 'receiver': receiver.id, 'map_mode': self.map_modes[input_['mm']].id, 'sw_mode': self.switch_modes[input_['sw']].id, 'freq': freq, 'freq_res': freq_res, 'zenith_angle_deg': zenith_angle_deg, 'is_dsb': (input_['sb'] == 'dsb'), 'dual_polarization': input_['dual_pol'], 'n_points': input_['n_pt'], 'dim_x': input_['dim_x'], 'dim_y': input_['dim_y'], 'dx': input_['dx'], 'dy': input_['dy'], 'basket_weave': input_['basket'], 'separate_offs': input_['sep_off'], 'continuum_mode': input_['cont'], 'with_extra_output': True, } try: if mode == self.CALC_TIME: (result, extra) = self.itc.calculate_time(input_['rms'], tau_225=input_['tau'], **kwargs) output = { 'elapsed': result / 3600.0, } elif mode == self.CALC_RMS_FROM_ELAPSED_TIME: (result, extra) = self.itc.calculate_rms_for_elapsed_time( input_['elapsed'] * 3600.0, tau_225=input_['tau'], **kwargs) output = { 'rms': result, } elif mode == self.CALC_RMS_FROM_INT_TIME: (result, extra) = self.itc.calculate_rms_for_int_time( input_['int_time'], tau_225=input_['tau'], **kwargs) output = { 'rms': result, 'elapsed': extra.pop('elapsed_time') / 3600.0, } else: raise CalculatorError('Unknown mode.') except HeterodyneITCError as e: raise UserError(e.message) except ZeroDivisionError: raise UserError( 'Division by zero error occurred during calculation.') except ValueError as e: if e.message == 'math domain error': raise UserError( 'Negative square root error occurred during calculation.') raise weather_band_comparison = OrderedDict() kwargs['with_extra_output'] = False for (weather_band, weather_band_info) in \ JCMTWeather.get_available().items(): weather_band_result = {} for condition_name in ('rep', 'min', 'max'): condition_tau = getattr(weather_band_info, condition_name) if condition_tau is None: weather_band_result[condition_name] = None continue condition_result = None try: if mode == self.CALC_TIME: condition_result = \ self.itc.calculate_time( input_['rms'], tau_225=condition_tau, **kwargs) / 3600.0 elif mode == self.CALC_RMS_FROM_ELAPSED_TIME: condition_result = \ self.itc.calculate_rms_for_elapsed_time( input_['elapsed'] * 3600.0, tau_225=condition_tau, **kwargs) elif mode == self.CALC_RMS_FROM_INT_TIME: condition_result = \ self.itc.calculate_rms_for_int_time( input_['int_time'], tau_225=condition_tau, **kwargs) except HeterodyneITCError as e: pass except ZeroDivisionError: pass except ValueError: pass weather_band_result[condition_name] = condition_result weather_band_comparison[weather_band] = weather_band_result primary_output = self.get_outputs(mode)[0] extra['wb_comparison'] = weather_band_comparison extra['wb_comparison_format'] = primary_output.format extra['wb_comparison_unit'] = primary_output.unit extra_output.update(extra) return CalculatorResult(output, extra_output) def condense_calculation(self, mode, version, calculation): self._condense_merge_values(calculation, (('pos', 'pos_type'), ('res', 'res_unit')))
class HeterodyneCalculator(JCMTCalculator): CALC_TIME = 1 CALC_RMS_FROM_ELAPSED_TIME = 2 CALC_RMS_FROM_INT_TIME = 3 modes = OrderedDict(( (CALC_TIME, CalculatorMode('time', 'Time required for target RMS')), (CALC_RMS_FROM_ELAPSED_TIME, CalculatorMode('rms_el', 'RMS expected in elapsed time')), (CALC_RMS_FROM_INT_TIME, CalculatorMode('rms_int', 'RMS for integration time per point')), )) _map_modes = OrderedDict(( ('grid', MappingMode(HeterodyneITC.GRID, 'Grid', None)), ('jiggle', MappingMode(HeterodyneITC.JIGGLE, 'Jiggle', None)), ('raster', MappingMode(HeterodyneITC.RASTER, 'Raster', None)), )) switch_modes = OrderedDict(( ('bmsw', SwitchingMode(HeterodyneITC.BMSW, 'Beam')), ('pssw', SwitchingMode(HeterodyneITC.PSSW, 'Position')), ('frsw', SwitchingMode(HeterodyneITC.FRSW, 'Frequency')), )) # Note: the JavaScript assumes that each array-only mode is preceeded # by the equivalent non-array-only mode. In other words, if an array-only # mode is selected when a non-array receiver is chosen, it should change # to the preceeding mode. acsis_modes = OrderedDict(( (1, ACSISMode('250 MHz', 0.0305, False)), (2, ACSISMode('400 MHz', 0.061, True)), (3, ACSISMode('1000 MHz', 0.488, False)), (4, ACSISMode('1600 MHz', 0.977, True)), )) version = 1 @classmethod def get_code(cls): """ Get the calculator "code". This is a short string used to uniquely identify the calculator within the facility which uses it. """ return 'heterodyne' def __init__(self, *args): """ Construct calculator. Calls the superclass constructor and then initializes a Heterodyne ITC object. """ super(HeterodyneCalculator, self).__init__(*args) self.itc = HeterodyneITC() # Determine which combinations of mapping and switching modes # are allowed. valid_modes = self.itc.get_valid_modes() self.map_modes = OrderedDict(( (map_code, map_mode._replace(sw_modes=[ sw_code for (sw_code, sw_mode) in self.switch_modes.items() if (map_mode.id, sw_mode.id) in valid_modes ])) for (map_code, map_mode) in self._map_modes.items() )) def get_name(self): return 'Heterodyne ITC' def get_calc_version(self): return self.itc.get_version() def get_inputs(self, mode, version=None): """ Get the list of calculator inputs for a given version of the calculator. """ if version is None: version = self.version inputs = SectionedList() if version == 1: inputs.extend([ CalculatorValue('rx', 'Receiver', 'Receiver', '{}', None), CalculatorValue('freq', 'Frequency', '\u03bd', '{:.3f}', 'GHz'), CalculatorValue('res', 'Spectral resolution', '\u0394\u03bd', '{:.4f}', None), CalculatorValue('res_unit', 'Resolution unit', '\u0394\u03bd unit', '{}', None), CalculatorValue('sb', 'Sideband mode', 'SB', '{}', None), CalculatorValue('dual_pol', 'Dual polarization', 'DP', '{}', None), CalculatorValue('cont', 'Continuum mode', 'CM', '{}', None), ], section='rx', section_name='Receiver') inputs.extend([ CalculatorValue('pos', 'Source position', 'Pos.', '{:.1f}', '\u00b0'), CalculatorValue('pos_type', 'Source position type', 'Pos. type', '{}', None), CalculatorValue('tau', '225 GHz opacity', '\u03c4\u2082\u2082\u2085', '{:.3f}', None), ], section='src', section_name='Source and Conditions') inputs.extend([ CalculatorValue('mm', 'Mapping mode', 'Mode', '{}', None), CalculatorValue('sw', 'Switching mode', 'Switch', '{}', None), CalculatorValue('n_pt', 'Number of points', 'Points', '{:d}', None), CalculatorValue('sep_off', 'Separate offs', 'SO', '{}', None), CalculatorValue('dim_x', 'Raster width', 'x', '{}', '"'), CalculatorValue('dim_y', 'Raster height', 'y', '{}', '"'), CalculatorValue('dx', 'Pixel width', 'dx', '{}', '"'), CalculatorValue('dy', 'Pixel/scan height', 'dy', '{}', '"'), CalculatorValue('basket', 'Basket weave', 'BW', '{}', None), ], section='obs', section_name='Observation') else: raise CalculatorError('Unknown version.') inputs.add_section('req', 'Requirement') if mode == self.CALC_TIME: if version == 1: inputs.extend([ CalculatorValue('rms', 'Target sensitivity', '\u03c3', '{:.3f}', 'K TA*'), ], section='req') else: raise CalculatorError('Unknown version.') elif mode == self.CALC_RMS_FROM_ELAPSED_TIME: if version == 1: inputs.extend([ CalculatorValue('elapsed', 'Elapsed time', 'Elapsed', '{:.3f}', 'hours'), ], section='req') else: raise CalculatorError('Unknown version.') elif mode == self.CALC_RMS_FROM_INT_TIME: if version == 1: inputs.extend([ CalculatorValue('int_time', 'Integration time', 'Int. time', '{:.3f}', 'seconds'), ], section='req') else: raise CalculatorError('Unknown version.') else: raise CalculatorError('Unknown mode.') return inputs def get_default_input(self, mode): """ Get the default input values (for the current version). """ common_inputs = [ ('rx', 'HARP'), ('mm', 'raster'), ('sw', 'pssw'), ('freq', 345.796), ('res', 0.488), ('res_unit', 'MHz'), ('tau', 0.1), ('pos', 40.0), ('pos_type', 'dec'), ('sb', 'ssb'), ('dual_pol', False), ('n_pt', 1), ('dim_x', 180), ('dim_y', 180), ('dx', 8), ('dy', 8), ('basket', False), ('sep_off', False), ('cont', False), ] if mode == self.CALC_TIME: return dict(common_inputs + [ ('rms', 1.0), ]) elif mode == self.CALC_RMS_FROM_ELAPSED_TIME: return dict(common_inputs + [ ('elapsed', 0.75), ]) elif mode == self.CALC_RMS_FROM_INT_TIME: return dict(common_inputs + [ ('int_time', 30.0), ]) else: raise CalculatorError('Unknown mode.') def format_input(self, inputs, values): """ Format input values for display in the input form. """ defaults = self.get_default_input(self.CALC_TIME) is_array_receiver = self.get_receiver_by_name( values['rx'], as_object=True).array is not None formatted_inputs = { x.code: x.format.format(values[x.code] if values[x.code] is not None else defaults.get(x.code)) if x.code not in ('rx', 'mm', 'sw', 'sb', 'dual_pol', 'n_pt', 'basket', 'sep_off', 'cont', 'res_unit') else (values[x.code] if values[x.code] is not None else defaults.get(x.code)) for x in inputs } acsis_mode = None if values['res_unit'] == 'MHz': for (acsis_mode_num, acsis_mode_info) in self.acsis_modes.items(): if acsis_mode_info.array_only and not is_array_receiver: continue if abs(values['res'] - acsis_mode_info.freq_res) < 0.0001: acsis_mode = acsis_mode_num break formatted_inputs.update({ 'tau_band': self.get_tau_band(values['tau']), 'acsis_mode': acsis_mode, 'dy_spacing': '{:.3f}'.format( values['dy'] if values['dy'] is not None else defaults['dy']), }) return formatted_inputs def get_form_input(self, inputs, form): """ Extract the input values from the submitted form. """ defaults = self.get_default_input(self.CALC_TIME) receiver = self.get_receiver_by_name(form['rx'], as_object=True) map_mode = self.map_modes[form['mm']].id values = {} for input_ in inputs: if input_.code in ('dual_pol', 'basket', 'sep_off', 'cont'): # Checkboxes: true if they appear in the form parameters. values[input_.code] = input_.code in form elif input_.code == 'tau': (tau, tau_band) = self.get_form_tau(form) values['tau'] = tau values['tau_band'] = tau_band elif input_.code == 'res': if form['acsis_mode'] == 'other': values[input_.code] = form[input_.code] values['acsis_mode'] = None else: acsis_mode = int(form['acsis_mode']) values[input_.code] = self.acsis_modes[acsis_mode].freq_res values['acsis_mode'] = acsis_mode elif input_.code == 'res_unit': if form['acsis_mode'] == 'other': values[input_.code] = form[input_.code] else: values[input_.code] = 'MHz' elif input_.code == 'n_pt': if map_mode == HeterodyneITC.GRID: try: values[input_.code] = int(form['n_pt']) except ValueError: # Prefer to convert to integer so that the value could # match a possible jiggle pattern point count, but if # that fails, leave as a string so that we can warn # as normal for a malformed value. values[input_.code] = form['n_pt'] elif map_mode == HeterodyneITC.JIGGLE: if receiver.array is None: values[input_.code] = int(form['n_pt_jiggle']) else: values[input_.code] = int( form['n_pt_jiggle_' + receiver.name]) else: values[input_.code] = defaults.get(input_.code, None) else: # Need to allow parameters not to exist when they can # be disabled (e.g. raster parameters in other modes). # For now, allow all input values to be filled from the # general defaults. TODO: be more specific here? value = form.get(input_.code, None) if value is None: value = defaults.get(input_.code, None) values[input_.code] = value values['dy_spacing'] = form.get('dy_spacing_' + receiver.name, None) if values['dy_spacing'] is None: values['dy_spacing'] = '{:.3f}'.format(defaults['dy']) return values def convert_input_mode(self, mode, new_mode, input_): """ Convert the inputs for one mode to form a suitable set of inputs for another mode. Only called if the mode is changed. """ new_input = input_.copy() result = self(mode, input_) if new_mode == self.CALC_TIME: if mode == self.CALC_RMS_FROM_ELAPSED_TIME: new_input['rms'] = result.output['rms'] del new_input['elapsed'] elif mode == self.CALC_RMS_FROM_INT_TIME: new_input['rms'] = result.output['rms'] del new_input['int_time'] else: raise CalculatorError('Impossible mode change.') elif new_mode == self.CALC_RMS_FROM_ELAPSED_TIME: if mode == self.CALC_TIME: new_input['elapsed'] = result.output['elapsed'] del new_input['rms'] elif mode == self.CALC_RMS_FROM_INT_TIME: new_input['elapsed'] = result.output['elapsed'] del new_input['int_time'] else: raise CalculatorError('Impossible mode change.') elif new_mode == self.CALC_RMS_FROM_INT_TIME: if mode == self.CALC_TIME: new_input['int_time'] = result.extra['int_time'] del new_input['rms'] elif mode == self.CALC_RMS_FROM_ELAPSED_TIME: new_input['int_time'] = result.extra['int_time'] del new_input['elapsed'] else: raise CalculatorError('Impossible mode change.') else: raise CalculatorError('Unknown mode.') return new_input def convert_input_version(self, mode, old_version, input_): """ Converts the inputs from an older version so that they can be used with the current version of the calculator. """ return input_ def get_outputs(self, mode, version=None): """ Get the list of calculator outputs for a given version of the calculator. """ if version is None: version = self.version if mode == self.CALC_TIME: if version == 1: return [ CalculatorValue( 'elapsed', 'Elapsed time', 'Elapsed', '{:.3f}', 'hours'), ] else: raise CalculatorError('Unknown version.') elif mode == self.CALC_RMS_FROM_ELAPSED_TIME: if version == 1: return [ CalculatorValue( 'rms', 'Sensitivity', '\u03c3', '{:.3f}', 'K TA*'), ] else: raise CalculatorError('Unknown version.') elif mode == self.CALC_RMS_FROM_INT_TIME: if version == 1: return [ CalculatorValue( 'rms', 'Sensitivity', '\u03c3', '{:.3f}', 'K TA*'), CalculatorValue( 'elapsed', 'Elapsed time', 'Elapsed', '{:.3f}', 'hours'), ] else: raise CalculatorError('Unknown version.') else: raise CalculatorError('Unknown mode.') def get_extra_context(self): """ Return extra information to be given to the view template. """ return { 'weather_bands': JCMTWeather.get_available(), 'receivers': HeterodyneReceiver.get_all_receivers().values(), 'map_modes': self.map_modes, 'switch_modes': self.switch_modes, 'jiggle_patterns': self.itc.get_jiggle_patterns(), 'acsis_modes': self.acsis_modes, 'int_time_minimum': self.itc.int_time_minimum, 'position_types': self.position_type, } def parse_input(self, mode, input_, defaults=None): """ Parse inputs as obtained from the HTML form (typically unicode) and return values suitable for calculation (perhaps float). """ receiver = self.get_receiver_by_name(input_['rx'], as_object=True) parsed = {} for field in self.get_inputs(mode): try: if field.code in ('freq', 'res', 'pos', 'rms', 'tau'): parsed[field.code] = float(input_[field.code]) elif field.code == 'dx': if receiver.array is None: parsed[field.code] = float(input_[field.code]) else: # The "dx" input is disabled for array receivers: # always use the pixel size. parsed[field.code] = receiver.pixel_size elif field.code == 'dy': if receiver.array is None: parsed[field.code] = float(input_[field.code]) else: parsed[field.code] = float(input_['dy_spacing']) elif field.code in ('dim_x', 'dim_y', 'int_time'): parsed[field.code] = float(input_[field.code]) elif field.code in ('n_pt'): parsed[field.code] = int(input_[field.code]) elif field.code == 'elapsed': parsed[field.code] = parse_time(input_[field.code]) else: parsed[field.code] = input_[field.code] except ValueError: if (not input_[field.code]) and (defaults is not None): parsed[field.code] = defaults[field.code] else: raise UserError('Invalid value for {}.', field.name) self._validate_position(parsed['pos'], parsed['pos_type']) map_mode = self.map_modes[parsed['mm']].id # Remove irrelevant input values for the given mode. if map_mode == HeterodyneITC.RASTER: parsed['n_pt'] = None else: parsed['dim_x'] = None parsed['dim_y'] = None parsed['dx'] = None parsed['dy'] = None return parsed def get_receiver_by_name(self, receiver_name, as_object=False): """ Get a receiver by name. """ for (rx_id, rx_info) in HeterodyneReceiver.get_all_receivers().items(): if rx_info.name == receiver_name: if as_object: return ReceiverInfoID(*rx_info, id=rx_id) return rx_id raise UserError('Receiver not recognised.') def __call__(self, mode, input_): """ Perform a calculation, taking an input dictionary and returning a CalculatorResult object. The result object contains the essential output, a dictionary with entries corresponding to the list given by get_outputs. It also contains any extra output for display but which would not be stored in the database as part of the calculation result. """ extra_output = {} if input_['pos_type'] == 'dec': zenith_angle_deg = self.itc.estimate_zenith_angle_deg( input_['pos']) elif input_['pos_type'] == 'zen': zenith_angle_deg = input_['pos'] elif input_['pos_type'] == 'el': zenith_angle_deg = 90.0 - input_['pos'] elif input_['pos_type'] == 'am': zenith_angle_deg = degrees(acos(1.0 / input_['pos'])) else: raise UserError('Unknown source position type.') if input_['pos_type'] != 'zen': extra_output['zenith_angle'] = zenith_angle_deg receiver = self.get_receiver_by_name(input_['rx'], as_object=True) freq = input_['freq'] freq_res = input_['res'] freq_res_unit = input_['res_unit'] if freq_res_unit == 'MHz': extra_output['res_velocity'] = \ self.itc.freq_res_to_velocity(freq, freq_res) elif freq_res_unit == 'km/s': freq_res = self.itc.velocity_to_freq_res(freq, freq_res) extra_output['res_freq'] = freq_res else: raise CalculatorError('Frequency units not recognised.') if freq < receiver.f_min: raise UserError('The frequency {} GHz is below the minimum ' 'frequency ({} GHz) of this receiver.', freq, receiver.f_min) elif freq > receiver.f_max: raise UserError('The frequency {} GHz is above the maximum ' 'frequency ({} GHz) of this receiver.', freq, receiver.f_max) kwargs = { 'receiver': receiver.id, 'map_mode': self.map_modes[input_['mm']].id, 'sw_mode': self.switch_modes[input_['sw']].id, 'freq': freq, 'freq_res': freq_res, 'zenith_angle_deg': zenith_angle_deg, 'is_dsb': (input_['sb'] == 'dsb'), 'dual_polarization': input_['dual_pol'], 'n_points': input_['n_pt'], 'dim_x': input_['dim_x'], 'dim_y': input_['dim_y'], 'dx': input_['dx'], 'dy': input_['dy'], 'basket_weave': input_['basket'], 'separate_offs': input_['sep_off'], 'continuum_mode': input_['cont'], 'with_extra_output': True, } try: if mode == self.CALC_TIME: (result, extra) = self.itc.calculate_time( input_['rms'], tau_225=input_['tau'], **kwargs) output = { 'elapsed': result / 3600.0, } elif mode == self.CALC_RMS_FROM_ELAPSED_TIME: (result, extra) = self.itc.calculate_rms_for_elapsed_time( input_['elapsed'] * 3600.0, tau_225=input_['tau'], **kwargs) output = { 'rms': result, } elif mode == self.CALC_RMS_FROM_INT_TIME: (result, extra) = self.itc.calculate_rms_for_int_time( input_['int_time'], tau_225=input_['tau'], **kwargs) output = { 'rms': result, 'elapsed': extra.pop('elapsed_time') / 3600.0, } else: raise CalculatorError('Unknown mode.') except HeterodyneITCError as e: raise UserError(e.message) except ZeroDivisionError: raise UserError( 'Division by zero error occurred during calculation.') except ValueError as e: if e.message == 'math domain error': raise UserError( 'Negative square root error occurred during calculation.') raise weather_band_comparison = OrderedDict() kwargs['with_extra_output'] = False for (weather_band, weather_band_info) in \ JCMTWeather.get_available().items(): weather_band_result = {} for condition_name in ('rep', 'min', 'max'): condition_tau = getattr(weather_band_info, condition_name) if condition_tau is None: weather_band_result[condition_name] = None continue condition_result = None try: if mode == self.CALC_TIME: condition_result = \ self.itc.calculate_time( input_['rms'], tau_225=condition_tau, **kwargs) / 3600.0 elif mode == self.CALC_RMS_FROM_ELAPSED_TIME: condition_result = \ self.itc.calculate_rms_for_elapsed_time( input_['elapsed'] * 3600.0, tau_225=condition_tau, **kwargs) elif mode == self.CALC_RMS_FROM_INT_TIME: condition_result = \ self.itc.calculate_rms_for_int_time( input_['int_time'], tau_225=condition_tau, **kwargs) except HeterodyneITCError as e: pass except ZeroDivisionError: pass except ValueError: pass weather_band_result[condition_name] = condition_result weather_band_comparison[weather_band] = weather_band_result primary_output = self.get_outputs(mode)[0] extra['wb_comparison'] = weather_band_comparison extra['wb_comparison_format'] = primary_output.format extra['wb_comparison_unit'] = primary_output.unit extra_output.update(extra) return CalculatorResult(output, extra_output) def condense_calculation(self, mode, version, calculation): self._condense_merge_values(calculation, (('pos', 'pos_type'), ('res', 'res_unit')))