def POWER(msg): ''' Enable or disable power to the cartridge. Single argument is ENABLE, which can be 1/0, on/off, true/false. Enabling power will trigger the demagnetization and defluxing sequence plus bias voltage error measurement, which could take considerable time. TODO: Invoking SIMULATE during this procedure (or interrupting it in some other way) could leave the cart powered but not setup properly. That is, not demagnetized or defluxed. Can we do anything about that? It would also be a problem if this task suddenly died, though, and there wouldn't be any indication. There will probably just need to be a procedure that an error during power up will require power-cycling the cartridge. ''' log.debug('POWER(%s)', msg.arg) args, kwargs = drama.parse_argument(msg.arg) enable = kwargs.get('ENABLE', '') or args[0] if hasattr(enable, 'lower'): enable = enable.lower().strip() enable = { '0': 0, 'off': 0, 'false': 0, '1': 1, 'on': 1, 'true': 1 }[enable] else: enable = int(bool(enable)) onoff = ['off', 'on'][enable] log.info('powering %s...', onoff) cart.power(enable) log.info('powered %s.', onoff)
def SET_SG_OUT(msg): '''Set Agilent output on (1) or off (0).''' log.debug('SET_SG_OUT(%s)', msg.arg) if not initialised: raise drama.BadStatus(drama.APP_ERROR, 'task needs INITIALISE') args, kwargs = drama.parse_argument(msg.arg) out = set_sg_out_args(*args, **kwargs) log.info('setting agilent output to %d', out) agilent.set_output(out) agilent.update(publish_only=True)
def SET_ATT(msg): '''Set Photonics attenuator counts (0-63, 0.5 dB per count).''' log.debug('SET_ATT(%s)', msg.arg) if not initialised: raise drama.BadStatus(drama.APP_ERROR, 'task needs INITIALISE') if not photonics: raise drama.BadStatus(drama.APP_ERROR, 'attenuator not in use') args, kwargs = drama.parse_argument(msg.arg) out = set_att_args(*args, **kwargs) log.info('setting attenuator counts to %d', att) photonics.set_attenuation(att)
def SET_SG_HZ(msg): '''Set Agilent output frequency in Hz.''' log.debug('SET_SG_HZ(%s)', msg.arg) if not initialised: raise drama.BadStatus(drama.APP_ERROR, 'task needs INITIALISE') args, kwargs = drama.parse_argument(msg.arg) hz = set_sg_hz_args(*args, **kwargs) if hz < 9e3 or hz > 32e9: raise drama.BadStatus(drama.INVARG, 'HZ %g outside [9 KHz, 32 GHz] range' % (hz)) log.info('setting agilent hz to %g', hz) agilent.set_hz(hz) agilent.update(publish_only=True)
def SET_SG_DBM(msg): '''Set Agilent output power in dBm.''' log.debug('SET_SG_DBM(%s)', msg.arg) if not initialised: raise drama.BadStatus(drama.APP_ERROR, 'task needs INITIALISE') args, kwargs = drama.parse_argument(msg.arg) dbm = set_sg_dbm_args(*args, **kwargs) if dbm < -130.0 or dbm > 0.0: raise drama.BadStatus(drama.INVARG, 'DBM %g outside [-130, 0] range' % (dbm)) log.info('setting agilent dbm to %g', dbm) agilent.set_dbm(dbm) agilent.update(publish_only=True)
def INITIALISE(msg): ''' Reinitialise (reinstantiate) the cartridge. ''' global cart, inifile, band log.debug('INITIALISE(%s)', msg.arg) args, kwargs = drama.parse_argument(msg.arg) if 'INITIALISE' in kwargs: inifile = kwargs['INITIALISE'] if not inifile: raise drama.BadStatus(drama.INVARG, 'missing argument INITIALISE, .ini file path') simulate = None if 'SIMULATE' in kwargs: simulate = int(kwargs['SIMULATE']) # bitmask if 'BAND' in kwargs: band = int(kwargs['BAND']) if not band: raise drama.BadStatus(drama.INVARG, 'missing argument BAND, receiver band number') # kick the update loop, if running, just to make sure it can't interfere # with cart's initialise(). try: drama.kick(taskname, "UPDATE").wait() except drama.DramaException: pass # we recreate the cart instance to force it to reread its ini file. # note that Cart.__init__() calls Cart.initialise(). log.info('initialising band %d...', band) del cart cart = None gc.collect() cart = namakanui.cart.Cart(band, inifile, drama.wait, drama.set_param, simulate) # set the SIMULATE bitmask used by the cart drama.set_param('SIMULATE', cart.simulate) # restart the update loop drama.blind_obey(taskname, "UPDATE") log.info('initialised.')
def LOAD_HOME(msg): '''Home the load stage. No arguments. NOTE: This can twist up any wires running to the load stage, e.g. for a tone source. Supervise as needed. ''' log.debug('LOAD_HOME') if not initialised: raise drama.BadStatus(drama.APP_ERROR, 'task needs INITIALISE') if msg.reason == drama.REA_OBEY: log.info('homing load...') load.home() log.info('load homed.') else: log.error('LOAD_HOME stopping load due to unexpected msg %s', msg) load.stop()
def LOAD_MOVE(msg): '''Move the load. Arguments: POSITION: Named position or absolute encoder counts. ''' log.debug('LOAD_MOVE(%s)', msg.arg) if not initialised: raise drama.BadStatus(drama.APP_ERROR, 'task needs INITIALISE') if msg.reason == drama.REA_OBEY: args, kwargs = drama.parse_argument(msg.arg) pos = load_move_args(*args, **kwargs) log.info('moving load to %s...', pos) load.move(pos) log.info('load at %d, %s.', load.state['pos_counts'], load.state['pos_name']) else: log.error('LOAD_MOVE stopping load due to unexpected msg %s', msg) load.stop()
def sequence(msg): ''' Callback called for every entry to the SEQUENCE action. This lets us place monitors on the Namakanui engineering tasks without starting a background action from CONFIGURE or SETUP_SEQUENCE. ''' log.debug('sequence: msg=%s', msg) if msg.reason == drama.REA_OBEY: sequence.start = drama.get_param('START') sequence.end = drama.get_param('END') sequence.dwell = drama.get_param('DWELL') sequence.step_counter = sequence.start sequence.state_table_index = 0 sequence.dwell_counter = 0 # start monitor on CART_TASK to track lock status. # TODO: do we need a faster update during obs? 5s is pretty slow. sequence.cart_tid = drama.monitor(CART_TASK, 'DYN_STATE') # start monitor on NAMAKANUI.LAKESHORE to get TEMP_AMBIENT sequence.lakeshore_tid = drama.monitor(NAMAKANUI_TASK, 'LAKESHORE') elif msg.reason == drama.REA_TRIGGER and msg.transid == sequence.cart_tid: if msg.status == drama.MON_STARTED: pass # lazy, just let the drama dispatcher clean up after us elif msg.status == drama.MON_CHANGED: # TODO: do we need to check other parameters also? if msg.arg['pll_unlock']: raise drama.BadStatus(WRAP__RXNOTLOCKED, 'lost lock during sequence') else: raise drama.BadStatus( msg.status, f'unexpected message for {CART_TASK}.DYN_STATE monitor: {msg}') elif msg.reason == drama.REA_TRIGGER and msg.transid == sequence.lakeshore_tid: if msg.status == drama.MON_STARTED: pass # lazy, just let the drama dispatcher clean up after us elif msg.status == drama.MON_CHANGED: g_state['TEMP_AMBIENT'] = msg.arg['temp5'] else: raise drama.BadStatus( msg.status, f'unexpected message for {NAMAKANUI_TASK}.LAKESHORE monitor: {msg}' )
def TUNE(msg): ''' Takes three arguments, LO_GHZ, VOLTAGE, and LOCK_ONLY. If VOLTAGE is not given, PLL control voltage will not be adjusted following the initial lock. If LOCK_ONLY is True, bias voltage, PA, LNA, and magnets will not be adjusted after locking the receiver. The reference signal and IF switch must already be set externally. ''' log.debug('TUNE(%s)', msg.arg) args, kwargs = drama.parse_argument(msg.arg) lo_ghz, voltage, lock_only = tune_args(*args, **kwargs) vstr = '' if voltage is not None: vstr += ', %g V' % (voltage) if lock_only: vstr += ', LOCK_ONLY' log.info('tuning to LO %g GHz%s...', lo_ghz, vstr) cart.tune(lo_ghz, voltage, lock_only=lock_only) log.info('tuned.')
def SET_BAND(msg): '''Set IFSwitch band to BAND. If this would change the selection, first sets Agilent to a safe level to avoid high power to mixer.''' log.debug('SET_BAND(%s)', msg.arg) if not initialised: raise drama.BadStatus(drama.APP_ERROR, 'task needs INITIALISE') args, kwargs = drama.parse_argument(msg.arg) band = set_band_args(*args, **kwargs) if band not in [3, 6, 7]: raise drama.BadStatus(drama.INVARG, 'BAND %d not one of [3,6,7]' % (band)) if ifswitch.get_band() != band: log.info('setting IF switch to band %d', band) # reduce power to minimum levels agilent.set_dbm(agilent.safe_dbm) agilent.update(publish_only=True) if photonics: photonics.set_attenuation(photonics.max_att) ifswitch.set_band(band) else: log.info('IF switch already at band %d', band)
def CART_POWER(msg): '''Power a cartridge on or off. Arguments: BAND: One of 3,6,7 ENABLE: Can be 1/0, on/off, true/false ''' log.debug('CART_POWER(%s)', msg.arg) if not initialised: raise drama.BadStatus(drama.APP_ERROR, 'task needs INITIALISE') args, kwargs = drama.parse_argument(msg.arg) band, enable = cart_power_args(*args, **kwargs) if band not in [3, 6, 7]: raise drama.BadStatus(drama.INVARG, 'BAND %d not one of [3,6,7]' % (band)) cartname = cartridge_tasknames[band] onoff = ['off', 'on'][enable] log.info('band %d powering %s...', band, onoff) msg = drama.obey(cartname, 'POWER', enable).wait() if msg.status != 0: raise drama.BadStatus(msg.status, '%s POWER %s failed' % (cartname, onoff)) log.info('band %d powered %s.', band, ['off', 'on'][enable])
def UPDATE(msg): ''' Keep overall cart.state updated with a 5s period. There are 3 update functions, so call update_one at 0.6Hz, every 1.67s. TODO: wrap this in a try block so it keeps going? what exceptions do we need to catch? ''' delay = 1.67 if msg.reason == drama.REA_KICK: log.debug('UPDATE kicked.') return if msg.reason == drama.REA_OBEY: log.debug('UPDATE started.') # INITIALISE has just done an update_all, so skip the first update. drama.reschedule(delay) return log.debug('UPDATE reschedule.') cart.update_one() # calls drama.set_param to publish state drama.reschedule(delay)
def UPDATE(msg): ''' Update local class instances every 10s. This is half the frequency of the nominal cryo update rate, but we don't expect state to change quickly. Try updating everything in one call since it is simpler than staggering. TODO: wrap this in a try/catch block? TODO: small delay between updates to let DRAMA message loop run? or stagger individual updates? ''' delay = 10 if msg.reason == drama.REA_KICK: log.debug('UPDATE kicked.') return if msg.reason == drama.REA_OBEY: log.debug('UPDATE started.') if not initialised: raise drama.BadStatus(drama.APP_ERROR, 'task needs INITIALISE') # INITIALISE has just updated everything, so skip the first update. drama.reschedule(delay) return log.debug('UPDATE reschedule.') # RMB 20200610: try each update function independently; keep UPDATE running. #cryo.update() #load.update() #agilent.update() #ifswitch.update() update_funcs = [cryo.update, load.update, agilent.update, ifswitch.update] if photonics: update_funcs.append(photonics.update) for f in update_funcs: try: f() except: # TODO limit bycatch log.exception('UPDATE exception') drama.reschedule(delay)
def initialise(msg): ''' Callback for the INITIALISE action. ''' log.info('initialise: msg=%s', msg) if msg.reason == drama.REA_OBEY: xmlname = msg.arg['INITIALISE'] initxml = drama.obj_from_xml(xmlname) drama.set_param('INITIALISE', initxml) if log.isEnabledFor(logging.DEBUG): # expensive formatting log.debug("INITIALISE:\n%s", pprint.pformat(initxml)) global g_esma_mode g_esma_mode = int(bool(msg.arg.get('ESMA_MODE', 0))) drama.set_param('ESMA_MODE', numpy.int32(g_esma_mode)) initxml = initxml['frontend_init'] inst = initxml['INSTRUMENT'] name = inst['NAME'] if name != taskname: raise drama.BadStatus( WRAP__WRONG_INSTRUMENT_NAME, 'got INSTRUMENT.NAME=%s instead of %s' % (name, taskname)) for i, r in enumerate(inst['receptor']): ID = r['id'] VAL = r['health'] # ON or OFF valid_ids = { 3: ['NA0', 'NA1'], 6: ['NU0L', 'NU1L', 'NU0U', 'NU1U'], 7: ['NW0L', 'NW1L', 'NW0U', 'NW1U'] } if ID not in valid_ids[g_band]: raise drama.BadStatus(WRAP__WRONG_RECEPTOR_IN_INITIALISE, 'bad INSTRUMENT.receptor.id %s' % (ID)) g_state['RECEPTOR_ID%d' % (i + 1)] = ID g_state['RECEPTOR_VAL%d' % (i + 1)] = VAL # fill in the static bits for first cell of STATE table cal = initxml['CALIBRATION'] t_cold = float(cal['T_COLD']) # K? t_spill = float(cal['T_SPILL']) t_hot = float(cal['T_HOT']) g_state['TEMP_LOAD2'] = t_cold g_state['TEMP_TSPILL'] = t_spill g_state['TEMP_AMBIENT'] = t_hot global t_cold_freq, t_cold_temp t_cold_table = cal['T_COLD_TABLE'] t_cold_freq = [float(x['FREQ']) for x in t_cold_table] t_cold_temp = [float(x['TEMP']) for x in t_cold_table] assert t_cold_freq == sorted(t_cold_freq) # TODO remove, not used manual = inst.get('TUNING', '').startswith('MANUAL') # skipping waveBand stuff # get name and path to our cartridge task global CART_TASK msg = drama.get(NAMAKANUI_TASK, 'TASKNAMES').wait(5) check_message(msg, f'get({NAMAKANUI_TASK},TASKNAMES)') CART_TASK = msg.arg['TASKNAMES'][f'B{g_band}'] drama.cache_path(CART_TASK) # get SIMULATE value and mask out the bits we don't care about msg = drama.get(NAMAKANUI_TASK, 'SIMULATE').wait(5) check_message(msg, f'get({NAMAKANUI_TASK},SIMULATE)') simulate = msg.arg['SIMULATE'] otherbands = [3, 6, 7] otherbands.remove(g_band) otherbits = 0 for band in otherbands: for bit in namakanui.sim.bits_for_band(band): otherbits |= bit simulate &= ~otherbits drama.set_param('SIMULATE', numpy.int32(simulate)) # might need to be 4-byte int # send load to AMBIENT, will fail if not already homed pos = f'b{g_band}_hot' log.info('moving load to %s...', pos) msg = drama.obey(NAMAKANUI_TASK, 'LOAD_MOVE', pos).wait(30) check_message(msg, f'obey({NAMAKANUI_TASK},LOAD_MOVE,{pos})') g_state['LOAD'] = 'AMBIENT' # power up the cartridge if necessary. this might take a little while. log.info('powering up band %d cartridge...', g_band) msg = drama.obey(NAMAKANUI_TASK, 'CART_POWER', g_band, 1).wait(30) check_message(msg, f'obey({NAMAKANUI_TASK},CART_POWER,{g_band},1)') # publish initial parameters; not sure if really needed. drama.set_param('LOAD', g_state['LOAD']) drama.set_param('STATE', [{'NUMBER': numpy.int32(0), **g_state}]) drama.set_param('TUNE_PAUSE', numpy.int32(0)) # TODO use this? drama.set_param('MECH_TUNE', numpy.int32(0)) drama.set_param('ELEC_TUNE', numpy.int32(0)) drama.set_param('LOCK_STATUS', numpy.int32(0)) drama.set_param('COMB_ON', numpy.int32(0)) # fesim only? drama.set_param('LO_FREQ', g_state['LO_FREQUENCY']) drama.set_param('REST_FREQUENCY', 0.0) drama.set_param('SIDEBAND', 'USB') drama.set_param('TEMP_TSPILL', t_spill) #drama.set_param('SB_MODE', {3:'SSB',6:'2SB',7:'2SB'}[g_band]) drama.set_param('SB_MODE', 'SSB') # debug # obey log.info('initialise done.')
def setup_sequence(msg, wait_set, done_set): ''' Callback for SETUP_SEQUENCE action. ''' log.debug('setup_sequence: msg=%s, wait_set=%s, done_set=%s', msg, wait_set, done_set) global g_sideband, g_rest_freq, g_center_freq, g_doppler global g_freq_mult, g_freq_off_scale global g_mech_tuning, g_elec_tuning, g_group, g_esma_mode if msg.reason == drama.REA_OBEY: # TODO these can probably be skipped init = drama.get_param('INITIALISE') init = init['frontend_init'] cal = init['CALIBRATION'] t_hot = float(cal['T_HOT']) t_cold = float(cal['T_COLD']) # TODO fast frequency switching state_table_name = msg.arg.get('FE_STATE', g_state['FE_STATE']) set_state_table(state_table_name) g_state['FE_STATE'] = state_table_name st = drama.get_param('MY_STATE_TABLE') state_index_size = int(st['size']) fe_state = st['FE_state'] if not isinstance(fe_state, dict): fe_state = fe_state[0] offset = float(fe_state['offset']) # for now, slow offset only if st['name'].startswith('OFFSET'): g_state['FREQ_OFFSET'] = offset else: g_state['FREQ_OFFSET'] = 0.0 setup_sequence.tune = False new_group = msg.arg.get('GROUP', g_group) if new_group != g_group and 'GROUP' in [g_mech_tuning, g_elec_tuning]: setup_sequence.tune = True g_group = new_group cd = ['CONTINUOUS', 'DISCRETE'] if g_mech_tuning in cd or g_elec_tuning in cd: setup_sequence.tune = True if g_esma_mode: setup_sequence.tune = False # if PTCS is in TASKS, get updated DOPPLER value -- # regardless of whether or not we're tuning the receiver. if ANTENNA_TASK in drama.get_param("TASKS").split(): wait_set.add(ANTENNA_TASK) else: drama.cache_path(ANTENNA_TASK) # shouldn't actually need this here # save time by moving load while waiting on doppler from ANTENNA_TASK load = msg.arg.get('LOAD', g_state['LOAD']) if load == 'LOAD2': # TODO: should this raise drama.BadStatus instead? # NOTE: LOAD in SDP/STATE still remains "LOAD2", # since otherwise the reducers will hang forever # waiting on LOAD2 data. log.warning('no LOAD2, setting to SKY instead') pos = 'b%d_sky' % (g_band) else: pos = 'b%d_%s' % (g_band, {'AMBIENT': 'hot', 'SKY': 'sky'}[load]) g_state['LOAD'] = '' # TODO unknown/invalid value log.info('moving load to %s...', pos) setup_sequence.load = load setup_sequence.load_tid = drama.obey(NAMAKANUI_TASK, 'LOAD_MOVE', pos) setup_sequence.load_target = f'obey({NAMAKANUI_TASK},LOAD_MOVE,{pos})' setup_sequence.load_timeout = time.time() + 30 drama.reschedule(setup_sequence.load_timeout) return {'MULT': numpy.int32(state_index_size)} # for JOS_MULT # obey elif msg.reason == drama.REA_RESCHED: # load must have timed out raise drama.BadStatus( drama.APP_TIMEOUT, f'Timeout waiting for {setup_sequence.load_target}') elif setup_sequence.load_tid is not None: if msg.transid != setup_sequence.load_tid: drama.reschedule(setup_sequence.load_timeout) return elif msg.reason != drama.REA_COMPLETE: raise drama.BadStatus( drama.UNEXPMSG, f'Unexpected reply to {setup_sequence.load_target}: {msg}') elif msg.status != 0: raise drama.BadStatus( msg.status, f'Bad status from {setup_sequence.load_target}') g_state['LOAD'] = setup_sequence.load # sdp should already hold the desired value, so no set_param here setup_sequence.load_tid = None if ANTENNA_TASK not in wait_set and ANTENNA_TASK not in done_set: done_set.add(ANTENNA_TASK) # once only #g_doppler = 1.0 # RMB 20200720: hold doppler at previous value elif ANTENNA_TASK in wait_set and ANTENNA_TASK in done_set: wait_set.remove(ANTENNA_TASK) # once only msg = drama.get(ANTENNA_TASK, 'RV_BASE').wait(5) check_message(msg, f'get({ANTENNA_TASK},RV_BASE)') rv_base = msg.arg['RV_BASE'] g_doppler = float(rv_base['DOPPLER']) else: # this is okay to wait on a single task... return g_state['DOPPLER'] = g_doppler if setup_sequence.tune: # tune the receiver. # TODO: target control voltage for fast frequency switching. lo_freq = (g_doppler * g_rest_freq) - (g_center_freq * g_freq_mult) + ( g_freq_off_scale * g_state['FREQ_OFFSET'] * 1e-3) voltage = 0.0 g_state['LOCKED'] = 'NO' drama.set_param('LOCK_STATUS', numpy.int32(0)) log.info('tuning receiver LO to %.9f GHz, %.3f V...', lo_freq, voltage) # RMB 20200714: try to hold output power steady while doppler tracking # by keeping the same tuning params (bias voltage, PA, etc). msg = drama.obey(NAMAKANUI_TASK, 'CART_TUNE', g_band, lo_freq, voltage, LOCK_ONLY=True).wait(30) check_message( msg, f'obey({NAMAKANUI_TASK},CART_TUNE,{g_band},{lo_freq},{voltage})') g_state['LO_FREQUENCY'] = lo_freq drama.set_param('LO_FREQ', lo_freq) g_state['LOCKED'] = 'YES' drama.set_param('LOCK_STATUS', numpy.int32(1)) else: # make sure rx is tuned and locked. better to fail here than in SEQUENCE. msg = drama.get(CART_TASK, 'DYN_STATE').wait(3) check_message(msg, f'get({CART_TASK},DYN_STATE)') dyn_state = msg.arg['DYN_STATE'] lo_freq = dyn_state['lo_ghz'] locked = int(not dyn_state['pll_unlock']) g_state['LO_FREQUENCY'] = lo_freq drama.set_param('LO_FREQ', lo_freq) g_state['LOCKED'] = ['NO', 'YES'][locked] drama.set_param('LOCK_STATUS', numpy.int32(locked)) if not lo_freq or not locked: raise drama.BadStatus(WRAP__RXNOTLOCKED, 'receiver unlocked in setup_sequence') # TODO: remove, we don't have a cold load t_cold = interpolate_t_cold(lo_freq) or g_state['TEMP_LOAD2'] g_state['TEMP_LOAD2'] = t_cold # TODO: get TEMP_TSPILL from NAMAKANUI and/or ENVIRO msg = drama.get(NAMAKANUI_TASK, 'LAKESHORE').wait(5) check_message(msg, f'get({NAMAKANUI_TASK},LAKESHORE)') g_state['TEMP_AMBIENT'] = msg.arg['LAKESHORE']['temp5'] log.info('setup_sequence done.')
def sequence_frame(frame): ''' Callback for every frame structure in RTS.STATE (endInt). Modify the passed frame in-place or return a dict to publish. Note that we assume a single-element STATE structure, so we just copy g_state into every frame. Even in batch mode this would still be correct, but excessive. TODO: How do we support fast frequency switching? The passed-in frame actually provides the following: NUMBER TAI_START TAI_END LAST_INTEG So we could keep g_state as a timeseries, and figure out the appropriate value for each frame. Some frames would have LOCKED=NO while the receiver tunes. Could probably support batch processing for free in that case, though I'd need to double-check the RTS/ACSIS code for how the feed-forward works -- does it only interpolate from the first frame, or from the last complete frame it saw? LAST_FREQ wouldn't necessarily be accurate either; there could be a few more frames on the current frequency depending on lag. Do we still need the LAST_FREQ field? Who uses it and how? ''' log.debug('sequence_frame: frame=%s', frame) # could compare this to start/end for first/last integration handling sequence.step_counter = frame['NUMBER'] # update frame with values that were used for this integration frame.update(g_state) # if state will change next frame, set up for it here. mst = drama.get_param('MY_STATE_TABLE') fe_state = mst['FE_state'] if isinstance(fe_state, dict): fe_state = [fe_state] old_sti = sequence.state_table_index sequence.dwell_counter += 1 if sequence.dwell_counter == sequence.dwell: sequence.dwell_counter = 0 sequence.state_table_index = (sequence.state_table_index + 1) % len(fe_state) # set LAST_FREQ if this was the last frame at this state table index if old_sti != sequence.state_table_index: frame['LAST_FREQ'] = numpy.int32(1) else: frame['LAST_FREQ'] = numpy.int32(0) # no tuning (fast frequency switching) in ESMA_MODE global g_esma_mode if g_esma_mode: return frame # skip the rest of this since fast frequency switching isn't supported yet. return frame if old_sti != sequence.state_table_index: # TODO: if we're tuning anyway, should we update doppler first? offset = float(fe_state[sequence.state_table_index]['offset']) g_state['FREQ_OFFSET'] = offset lo_freq = (g_doppler * g_rest_freq) - (g_center_freq * g_freq_mult) + ( g_freq_off_scale * offset * 1e-3) # TODO: target control voltage for fast frequency switching. voltage = 0.0 g_state['LOCKED'] = 'NO' drama.set_param('LOCK_STATUS', numpy.int32(0)) drama.set_param('LO_FREQ', lo_freq) log.info('tuning receiver LO to %.9f GHz, %.3f V...', lo_freq, voltage) msg = drama.obey(NAMAKANUI_TASK, 'CART_TUNE', g_band, lo_freq, voltage).wait(30) check_message( msg, f'obey({NAMAKANUI_TASK},CART_TUNE,{g_band},{lo_freq},{voltage})') g_state['LO_FREQUENCY'] = lo_freq g_state['LOCKED'] = 'YES' drama.set_param('LOCK_STATUS', numpy.int32(1)) # TODO: remove, we don't have a cold load t_cold = interpolate_t_cold(lo_freq) or g_state['TEMP_LOAD2'] g_state['TEMP_LOAD2'] = t_cold
def INITIALISE(msg): ''' Start the cartridge tasks and initialise them, then initialise the local control classes. Arguments: INITIALISE: The ini file path SIMULATE: Bitmask. If given, overrides config file settings. ''' global initialised, inifile, agilent, cryo, load, ifswitch, photonics global cartridge_tasknames, cold_mult, warm_mult log.debug('INITIALISE(%s)', msg.arg) args, kwargs = drama.parse_argument(msg.arg) initialised = False if 'INITIALISE' in kwargs: inifile = kwargs['INITIALISE'] if not inifile: raise drama.BadStatus(drama.INVARG, 'missing argument INITIALISE, .ini file path') simulate = None if 'SIMULATE' in kwargs: simulate = int(kwargs['SIMULATE']) config = IncludeParser(inifile) nconfig = config['namakanui'] cartridge_tasknames[3] = nconfig['b3_taskname'] cartridge_tasknames[6] = nconfig['b6_taskname'] cartridge_tasknames[7] = nconfig['b7_taskname'] # export these so the frontend task doesn't have to guess drama.set_param('TASKNAMES', {'B%d' % (k): v for k, v in cartridge_tasknames.items()}) # start the cartridge tasks in the background. # will exit immediately if already running, which is fine. log.info('starting cartridge tasks') subprocess.Popen([binpath + 'cartridge_task.py', cartridge_tasknames[3]]) subprocess.Popen([binpath + 'cartridge_task.py', cartridge_tasknames[6]]) subprocess.Popen([binpath + 'cartridge_task.py', cartridge_tasknames[7]]) # kill the UPDATE action while we fire things up try: drama.kick(taskname, "UPDATE").wait() except drama.DramaException: pass # kludge: sleep a short time to let cartridge tasks run up log.info('sleeping 3s for cartridge task startup') drama.wait(3) # TODO: do the ini file names really need to be configurable? # probably a bit overkill. cart_kwargs = {} if simulate is not None: cart_kwargs["SIMULATE"] = simulate for band in [3, 6, 7]: task = cartridge_tasknames[band] ini = datapath + nconfig['b%d_ini' % (band)] log.info('initialising %s', task) msg = drama.obey(task, "INITIALISE", BAND=band, INITIALISE=ini, **cart_kwargs).wait() if msg.status != 0: raise drama.BadStatus(msg.status, task + ' INITIALISE failed') # setting agilent frequency requires warm/cold multipliers for each band. # TODO: this assumes pubname=DYN_STATE -- could instead [include] config. # also this is rather a large get() for just a couple values. for band in [3, 6, 7]: dyn_state = drama.get(cartridge_tasknames[band], "DYN_STATE").wait().arg["DYN_STATE"] cold_mult[band] = dyn_state['cold_mult'] warm_mult[band] = dyn_state['warm_mult'] # now reinstantiate the local stuff if load is not None: load.close() del agilent del cryo del load del ifswitch del photonics agilent = None cryo = None load = None ifswitch = None photonics = None gc.collect() agilent = namakanui.agilent.Agilent(datapath + nconfig['agilent_ini'], drama.wait, drama.set_param, simulate) cryo = namakanui.cryo.Cryo(datapath + nconfig['cryo_ini'], drama.wait, drama.set_param, simulate) # wait a moment for load, jcms4 is fussy about reconnects drama.wait(1) load = namakanui.load.Load(datapath + nconfig['load_ini'], drama.wait, drama.set_param, simulate) ifswitch = namakanui.ifswitch.IFSwitch(datapath + nconfig['ifswitch_ini'], drama.wait, drama.set_param, simulate) if 'photonics_ini' in nconfig: photonics = namakanui.photonics.Photonics( datapath + nconfig['photonics_ini'], drama.wait, drama.set_param, simulate) # publish the load.positions table for the GUI drama.set_param('LOAD_TABLE', load.positions) # rebuild the simulate bitmask from what was actually set simulate = agilent.simulate | cryo.simulate | load.simulate | ifswitch.simulate | ( photonics.simulate if photonics else 0) for band in [3, 6, 7]: task = cartridge_tasknames[band] simulate |= drama.get(task, 'SIMULATE').wait(5).arg['SIMULATE'] drama.set_param('SIMULATE', simulate) # restart the update loop drama.blind_obey(taskname, "UPDATE") # TODO: power up the cartridges? tune? leave it for the FE wrapper? initialised = True log.info('initialised.')
def CART_TUNE(msg): '''Tune a cartridge, after setting reference frequency. Arguments: BAND: One of 3,6,7 LO_GHZ: Local oscillator frequency in gigahertz VOLTAGE: Desired PLL control voltage, [-10,10]. If not given, voltage will not be adjusted following the initial lock. LOCK_ONLY: if True, bias voltage, PA, LNA, and magnets will not be adjusted after locking the receiver. TODO: lock polarity (below or above reference) could be a parameter. for now we just read back from the cartridge task. TODO: save dbm offset and use it for close frequencies. ''' log.debug('CART_TUNE(%s)', msg.arg) if not initialised: raise drama.BadStatus(drama.APP_ERROR, 'task needs INITIALISE') args, kwargs = drama.parse_argument(msg.arg) band, lo_ghz, voltage, lock_only = cart_tune_args(*args, **kwargs) if band not in [3, 6, 7]: raise drama.BadStatus(drama.INVARG, 'BAND %d not one of [3,6,7]' % (band)) if not 70 <= lo_ghz <= 400: # TODO be more specific raise drama.BadStatus(drama.INVARG, 'LO_GHZ %g not in [70,400]' % (lo_ghz)) if voltage and not -10 <= voltage <= 10: raise drama.BadStatus(drama.INVARG, 'VOLTAGE %g not in [-10,10]' % (voltage)) if ifswitch.get_band() != band: log.info('setting IF switch to band %d', band) # reduce power first agilent.set_dbm(agilent.safe_dbm) if photonics: photonics.set_attenuation(photonics.max_att) ifswitch.set_band(band) cartname = cartridge_tasknames[band] # TODO don't assume pubname is DYN_STATE dyn_state = drama.get(cartname, "DYN_STATE").wait().arg["DYN_STATE"] lock_polarity = dyn_state['pll_sb_lock'] # 0=below_ref, 1=above_ref lock_polarity = -2.0 * lock_polarity + 1.0 fyig = lo_ghz / (cold_mult[band] * warm_mult[band]) fsig = (fyig * warm_mult[band] + agilent.floog * lock_polarity) / agilent.harmonic if photonics: dbm = agilent.interp_dbm(0, fsig) att = photonics.interp_attenuation(band, lo_ghz) log.info('setting photonics attenuator to %d counts', att) else: dbm = agilent.interp_dbm(band, lo_ghz) dbm = min(dbm, agilent.max_dbm) log.info('setting agilent to %g GHz, %g dBm', fsig, dbm) # set power safely while maintaining lock, if possible. hz = fsig * 1e9 if photonics: if att < photonics.state['attenuation']: agilent.set_hz_dbm(hz, dbm) photonics.set_attenuation(att) else: photonics.set_attenuation(att) agilent.set_hz_dbm(hz, dbm) else: agilent.set_hz_dbm(hz, dbm) agilent.set_output(1) agilent.update(publish_only=True) time.sleep(0.05) # wait 50ms; for small changes PLL might hold lock vstr = '' band_kwargs = {"LO_GHZ": lo_ghz} if voltage is not None: vstr += ', %g V' % (voltage) band_kwargs["VOLTAGE"] = voltage if lock_only: vstr += ', LOCK_ONLY' band_kwargs["LOCK_ONLY"] = lock_only log.info('band %d tuning to LO %g GHz%s...', band, lo_ghz, vstr) pll_if_power = 0.0 # tune in a loop, adjusting signal to get pll_if_power in proper range. # adjust attenuator if present, then adjust output dBm. if photonics: orig_att = att att_min = max(0, att - 24) # limit 2x nominal power tries = 0 max_tries = 5 while True: tries += 1 msg = drama.obey(cartname, 'TUNE', **band_kwargs).wait() if msg.reason != drama.REA_COMPLETE: raise drama.BadStatus(drama.UNEXPMSG, '%s bad TUNE msg: %s' % (cartname, msg)) elif msg.status == drama.INVARG: # frequency out of range agilent.set_dbm(agilent.safe_dbm) photonics.set_attenuation(photonics.max_att) raise drama.BadStatus(msg.status, '%s TUNE failed' % (cartname)) elif msg.status != 0: # tune failure, raise the power and try again old_att = att att -= 8 if att < att_min: att = att_min if att == old_att or tries > max_tries: # stuck at the limit, time to give up photonics.set_attenuation(orig_att) raise drama.BadStatus(msg.status, '%s TUNE failed' % (cartname)) log.warning( 'band %d tune failed, retuning at %d attenuator counts...', band, att) photonics.set_attenuation(att) time.sleep(0.05) continue # we got a lock. check the pll_if_power level. # TODO don't assume pubname is DYN_STATE dyn_state = drama.get(cartname, "DYN_STATE").wait().arg["DYN_STATE"] pll_if_power = dyn_state['pll_if_power'] if tries > max_tries: # avoid bouncing around forever break old_att = att if pll_if_power < -2.5: # power too high att += 4 elif pll_if_power > -0.7: # power too low att -= 4 else: # power is fine break if att < att_min: att = att_min elif att > photonics.max_att: att = photonics.max_att if att == old_att: # stuck at the limit, so it'll have to do break log.warning( 'band %d bad pll_if_power %.2f; retuning at %d attenuator counts...', band, pll_if_power, att) photonics.set_attenuation(att) time.sleep(0.05) log.info( 'band %d tuned to LO %g GHz, pll_if_power %.2f at %.2f dBm, %d attenuator counts', band, lo_ghz, pll_if_power, dbm, att) #else: if not (-2.5 <= pll_if_power <= -0.7): # adjust dbm after photonics if needed orig_dbm = dbm orig_att = att if photonics else 0 dbm_max = min(agilent.max_dbm, dbm + 3.0) # limit to 2x nominal power att_min = max(0, att - 6) if photonics else 0 # ditto tries = 0 max_tries = 5 while True: tries += 1 msg = drama.obey(cartname, 'TUNE', **band_kwargs).wait() if msg.reason != drama.REA_COMPLETE: raise drama.BadStatus(drama.UNEXPMSG, '%s bad TUNE msg: %s' % (cartname, msg)) elif msg.status == drama.INVARG: # frequency out of range agilent.set_dbm(agilent.safe_dbm) raise drama.BadStatus(msg.status, '%s TUNE failed' % (cartname)) elif msg.status != 0: # tune failure, raise the power and try again old_dbm = dbm dbm += 1.0 if dbm > dbm_max: dbm = dbm_max if dbm == old_dbm or tries > max_tries: # stuck at the limit, time to give up agilent.set_dbm(orig_dbm) raise drama.BadStatus(msg.status, '%s TUNE failed' % (cartname)) log.warning('band %d tune failed, retuning at %.2f dBm...', band, dbm) agilent.set_dbm(dbm) time.sleep(0.05) continue # we got a lock. check the pll_if_power level. # TODO don't assume pubname is DYN_STATE dyn_state = drama.get(cartname, "DYN_STATE").wait().arg["DYN_STATE"] pll_if_power = dyn_state['pll_if_power'] if tries > max_tries: # avoid bouncing around forever break old_dbm = dbm if pll_if_power < -2.5: # power too high dbm -= 0.5 elif pll_if_power > -0.7: # power too low dbm += 0.5 else: # power is fine break if dbm > dbm_max: dbm = dbm_max elif dbm < -20.0: dbm = -20.0 if dbm == old_dbm: # stuck at the limit, so it'll have to do break log.warning( 'band %d bad pll_if_power %.2f; retuning at %.2f dBm...', band, pll_if_power, dbm) agilent.set_dbm(dbm) time.sleep(0.05) log.info('band %d tuned to LO %g GHz, pll_if_power %.2f at %.2f dBm.', band, lo_ghz, pll_if_power, dbm)
def configure(msg, wait_set, done_set): ''' Callback for the CONFIGURE action. ''' log.debug('configure: msg=%s, wait_set=%s, done_set=%s', msg, wait_set, done_set) global g_sideband, g_rest_freq, g_center_freq, g_doppler global g_freq_mult, g_freq_off_scale global g_mech_tuning, g_elec_tuning, g_group, g_esma_mode if msg.reason == drama.REA_OBEY: config = drama.get_param('CONFIGURATION') if log.isEnabledFor(logging.DEBUG): # expensive formatting log.debug("CONFIGURATION:\n%s", pprint.pformat(config)) config = config['OCS_CONFIG'] fe = config['FRONTEND_CONFIG'] init = drama.get_param('INITIALISE') init = init['frontend_init'] inst = init['INSTRUMENT'] # init/config must have same number/order of receptors for i, (ir, cr) in enumerate(zip(inst['receptor'], fe['RECEPTOR_MASK'])): iid = ir['id'] cid = cr['RECEPTOR_ID'] if iid != cid: raise drama.BadStatus( WRAP__WRONG_RECEPTOR_IN_CONFIGURE, f'configure RECEPTOR_MASK[{i}].RECEPTOR_ID={cid} but initialise receptor[{i}].id={iid}' ) ival = ir['health'] cval = cr['VALUE'] if cval == 'NEED' and ival != 'ON': raise drama.BadStatus( WRAP__NEED_BAD_RECEPTOR, f'{cid}: configure RECEPTOR_MASK.VALUE={cval} but initialise receptor.health={ival}' ) if cval == 'ON' and ival == 'OFF': raise drama.BadStatus( WRAP__ON_RECEPTOR_IS_OFF, f'{cid}: configure RECEPTOR_MASK.VALUE={cval} but initialise receptor.health={ival}' ) if cval == 'OFF': g_state['RECEPTOR_VAL%d' % (i + 1)] = 'OFF' else: g_state['RECEPTOR_VAL%d' % (i + 1)] = ival g_sideband = fe['SIDEBAND'] if g_sideband not in ['USB', 'LSB']: raise drama.BadStatus(WRAP__UNKNOWN_SIDEBAND, f'SIDEBAND={g_sideband}') g_rest_freq = float(fe['REST_FREQUENCY']) g_freq_mult = {'USB': 1.0, 'LSB': -1.0}[g_sideband] # use IF_CENTER_FREQ from config file instead of initialise #g_center_freq = float(inst['IF_CENTER_FREQ']) g_center_freq = float(config['INSTRUMENT']['IF_CENTER_FREQ']) g_freq_off_scale = float(fe['FREQ_OFF_SCALE']) # MHz dtrack = fe['DOPPLER_TRACK'] g_mech_tuning = dtrack['MECH_TUNING'] g_elec_tuning = dtrack['ELEC_TUNING'] drama.set_param( 'MECH_TUNE', numpy.int32({ 'NEVER': 0, 'NONE': 0 }.get(g_mech_tuning, 1))) drama.set_param( 'ELEC_TUNE', numpy.int32({ 'NEVER': 0, 'NONE': 0 }.get(g_elec_tuning, 1))) drama.set_param('REST_FREQUENCY', g_rest_freq) drama.set_param('SIDEBAND', g_sideband) drama.set_param('SB_MODE', fe['SB_MODE']) #drama.set_param('SB_MODE', 'SSB') # debug # RMB 20200701: tune here even for CONTINUOUS/DISCRETE, # since the DCMs probably level themselves in CONFIGURE. og = ['ONCE', 'GROUP', 'CONTINUOUS', 'DISCRETE'] if g_esma_mode: configure.tune = False wait_set.add(ANTENNA_TASK) elif g_mech_tuning in og or g_elec_tuning in og: configure.tune = True wait_set.add(ANTENNA_TASK) else: configure.tune = False drama.cache_path(ANTENNA_TASK) # we can save a bit of time by moving the load to AMBIENT # while waiting for the ANTENNA_TASK to supply the doppler value. # TODO: is this really necessary? if configure.tune: pos = f'b{g_band}_hot' g_state['LOAD'] = '' # TODO unknown/invalid value; drama.set_param log.info('moving load to %s...', pos) configure.load_tid = drama.obey(NAMAKANUI_TASK, 'LOAD_MOVE', pos) configure.load_target = f'obey({NAMAKANUI_TASK},LOAD_MOVE,{pos})' configure.load_timeout = time.time() + 30 drama.reschedule(configure.load_timeout) return else: configure.load_tid = None # obey elif msg.reason == drama.REA_RESCHED: # load must have timed out raise drama.BadStatus(drama.APP_TIMEOUT, f'Timeout waiting for {configure.load_target}') elif configure.load_tid is not None: if msg.transid != configure.load_tid: drama.reschedule(configure.load_timeout) return elif msg.reason != drama.REA_COMPLETE: raise drama.BadStatus( drama.UNEXPMSG, f'Unexpected reply to {configure.load_target}: {msg}') elif msg.status != 0: raise drama.BadStatus(msg.status, f'Bad status from {configure.load_target}') g_state['LOAD'] = 'AMBIENT' drama.set_param('LOAD', g_state['LOAD']) configure.load_tid = None # TODO: figure out how to generalize this pattern to more obeys. if ANTENNA_TASK not in wait_set and ANTENNA_TASK not in done_set: done_set.add(ANTENNA_TASK) # once only g_doppler = 1.0 elif ANTENNA_TASK in wait_set and ANTENNA_TASK in done_set: wait_set.remove(ANTENNA_TASK) # once only msg = drama.get(ANTENNA_TASK, 'RV_BASE').wait(5) check_message(msg, f'get({ANTENNA_TASK},RV_BASE)') rv_base = msg.arg['RV_BASE'] g_doppler = float(rv_base['DOPPLER']) else: # this is okay to wait on a single task... return g_state['DOPPLER'] = g_doppler if configure.tune: # tune the receiver. # TODO: target control voltage for fast frequency switching. lo_freq = g_doppler * g_rest_freq - g_center_freq * g_freq_mult voltage = 0.0 g_state['LOCKED'] = 'NO' drama.set_param('LOCK_STATUS', numpy.int32(0)) log.info('tuning receiver LO to %.9f GHz, %.3f V...', lo_freq, voltage) msg = drama.obey(NAMAKANUI_TASK, 'CART_TUNE', g_band, lo_freq, voltage).wait(30) check_message( msg, f'obey({NAMAKANUI_TASK},CART_TUNE,{g_band},{lo_freq},{voltage})') g_state['LOCKED'] = 'YES' drama.set_param('LOCK_STATUS', numpy.int32(1)) else: # ask the receiver for its current status. # NOTE: no lo_ghz, or being unlocked, is not necessarily an error here. msg = drama.get(CART_TASK, 'DYN_STATE').wait(3) check_message(msg, f'get({CART_TASK},DYN_STATE)') dyn_state = msg.arg['DYN_STATE'] lo_freq = dyn_state['lo_ghz'] locked = int(not dyn_state['pll_unlock']) g_state['LOCKED'] = ['NO', 'YES'][locked] drama.set_param('LOCK_STATUS', numpy.int32(locked)) g_state['LO_FREQUENCY'] = lo_freq drama.set_param('LO_FREQ', lo_freq) # TODO: remove, we don't have a cold load t_cold = interpolate_t_cold(lo_freq) or g_state['TEMP_LOAD2'] g_state['TEMP_LOAD2'] = t_cold # TODO: do something with g_group? # it seems silly to potentially retune immediately in SETUP_SEQUENCE. log.info('configure done.')