def check_message(msg, target): '''Check msg after a wait() and raise errors if needed.''' if msg.reason == drama.REA_RESCHED: raise drama.BadStatus(drama.APP_TIMEOUT, f'Timeout waiting for {target}') elif msg.reason != drama.REA_COMPLETE: raise drama.BadStatus(drama.UNEXPMSG, f'Unexpected reply to {target}: {msg}') elif msg.status != 0: raise drama.BadStatus(msg.status, f'Bad status from {target}')
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 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 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 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 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 MAIN(msg): try: log.info('getting YFACTOR...') # be careful here #yfactor = drama.get('IFTASK@if-micro', 'YFACTOR').wait(1).arg['YFACTOR'] msg = drama.get('IFTASK@if-micro', 'YFACTOR').wait(1) if msg.reason != drama.REA_COMPLETE: raise drama.BadStatus(msg.status or drama.APP_ERROR, 'bad reply: %s' % (msg)) yfactor = msg.arg['YFACTOR'] log.info('getting ambient temperature...') jamb = epics.caget('nmnCryo:ls:temp5') jcold = 80.0 # AVG_PWR, LOW_PWR, HIGH_PWR, Y_FAC should already be numpy arrays log.info('J: %.3f, %.3f', jcold, jamb) log.info('AVG_SIZE:\n%s', yfactor['AVG_SIZE']) log.info('LOW_SIZE:\n%s', yfactor['LOW_SIZE']) log.info('HIGH_SIZE:\n%s', yfactor['HIGH_SIZE']) log.info('AVG_PWR:\n%s', yfactor['AVG_PWR']) log.info('HIGH_PWR:\n%s', yfactor['HIGH_PWR']) log.info('LOW_PWR:\n%s', yfactor['LOW_PWR']) log.info('Y_FAC:\n%s', yfactor['Y_FAC']) # why does this differ from Y_FAC? how is Y_FAC calculated? # PWR might be in dbm, in which case Y = 10**((HIGH-LOW)/10) #my_y = yfactor['HIGH_PWR'] / yfactor['LOW_PWR'] #log.info('MY_Y:\n%s', my_y) # this one is correct, so no more reason to print it out #my_y2 = 10**(0.1*(yfactor['HIGH_PWR']-yfactor['LOW_PWR'])) #log.info('MY_Y2:\n%s', my_y2) y = yfactor['Y_FAC'] trx = (jamb - y * jcold) / (y - 1) # zero out any spots with no samples trx *= yfactor['AVG_SIZE'].astype(bool).astype(float) log.info('trx:\n%s', trx) # print out stats/summary in blocks of 4 i = 0 n = 4 while i < len(trx): b = trx[i:i + n] log.info('dcm %2d-%2d: %s avg: %.2f +- %.2f', i, i + n - 1, b.round(2), b.mean(), b.std()) i += n finally: drama.Exit('done') # no need to raise instance
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 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): ''' 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.')
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.')