예제 #1
0
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)
예제 #2
0
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)
예제 #3
0
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)
예제 #4
0
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)
예제 #5
0
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)
예제 #6
0
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.')
예제 #7
0
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()
예제 #8
0
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()
예제 #9
0
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}'
            )
예제 #10
0
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.')
예제 #11
0
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)
예제 #12
0
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])
예제 #13
0
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)
예제 #14
0
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)
예제 #15
0
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.')
예제 #16
0
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.')
예제 #17
0
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
예제 #18
0
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.')
예제 #19
0
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)
예제 #20
0
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.')