def setup_sensors(self):
        """
        @brief    Set up monitoring sensors.
        """
        self._target_sensors = []
        for beam in self._beam_manager.get_beams():
            sensor = Sensor.string("{}-target".format(beam.idx),
                                   description="Target for beam {}".format(
                                       beam.idx),
                                   default=beam.target.format_katcp(),
                                   initial_status=Sensor.UNKNOWN)
            self.add_sensor(sensor)
            beam.register_observer(lambda beam, sensor=sensor: sensor.
                                   set_value(beam.target.format_katcp()))

        antenna_map = {
            a.name: a.format_katcp()
            for a in self._beam_manager.antennas
        }
        self._antennas_sensor = Sensor.string(
            "antennas",
            description=
            "JSON breakdown of the antennas (in KATPOINT format) associated with this delay engine",
            default=json.dumps(antenna_map),
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._antennas_sensor)

        self._phase_reference_sensor = Sensor.string(
            "phase-reference",
            description=
            "A KATPOINT target string denoting the F-engine phasing centre",
            default="unset,radec,0,0",
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._phase_reference_sensor)

        reference_antenna = Antenna(
            "reference,{ref.lat},{ref.lon},{ref.elev}".format(
                ref=self._beam_manager.antennas[0].ref_observer))
        self._reference_antenna_sensor = Sensor.string(
            "reference-antenna",
            description=
            "A KATPOINT antenna string denoting the reference antenna",
            default=reference_antenna.format_katcp(),
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._reference_antenna_sensor)
Ejemplo n.º 2
0
class DelayBufferController(object):
    def __init__(self, delay_client, ordered_beams, ordered_antennas, nreaders):
        """
        @brief    Controls shared memory delay buffers that are accessed by one or more
                  beamformer instances.

        @params   delay_client       A KATCPResourceClient connected to an FBFUSE delay engine server
        @params   ordered_beams      A list of beam IDs in the order that they should be generated by the beamformer
        @params   orderded_antennas  A list of antenna IDs in the order which they should be captured by the beamformer
        @params   nreaders           The number of posix shared memory readers that will access the memory
                                     buffers that are managed by this instance.
        """
        self._nreaders = nreaders
        self._delay_client = delay_client
        self._ordered_antennas = ordered_antennas
        self._ordered_beams = ordered_beams
        self.shared_buffer_key = "delay_buffer"
        self.mutex_semaphore_key = "delay_buffer_mutex"
        self.counting_semaphore_key = "delay_buffer_count"
        self._nbeams = len(self._ordered_beams)
        self._nantennas = len(self._ordered_antennas)
        self._delays_array = self._delays = np.rec.recarray((self._nbeams, self._nantennas),
            dtype=[("delay_rate","float32"),("delay_offset","float32")])
        self._targets = OrderedDict()
        for beam in self._ordered_beams:
            self._targets[beam] = Target(DEFAULT_TARGET)
        self._phase_reference = Target(DEFAULT_TARGET)
        self._update_rate = DEFAULT_UPDATE_RATE
        self._delay_span = DEFAULT_DELAY_SPAN
        self._update_callback = None
        self._beam_callbacks = {}

    def unlink_all(self):
        """
        @brief   Unlink (remove) all posix shared memory sections and semaphores.
        """
        log.debug("Unlinking all relevant posix shared memory segments and semaphores")
        try:
            posix_ipc.unlink_semaphore(self.counting_semaphore_key)
        except posix_ipc.ExistentialError:
            pass
        try:
            posix_ipc.unlink_semaphore(self.mutex_semaphore_key)
        except posix_ipc.ExistentialError:
            pass
        try:
            posix_ipc.unlink_shared_memory(self.shared_buffer_key)
        except posix_ipc.ExistentialError:
            pass

    @coroutine
    def fetch_config_info(self):
        """
        @brief   Retrieve configuration information from the delay configuration server
        """
        log.debug("Fetching configuration information from the delay configuration server")
        yield self._delay_client.until_synced()
        antennas_json = yield self._delay_client.sensor.antennas.get_value()
        try:
            antennas = json.loads(antennas_json)
        except Exception as error:
            log.exception("Failed to parse antennas")
            raise error
        self._antennas = [Antenna(antennas[antenna]) for antenna in self._ordered_antennas]
        log.debug("Ordered the antenna capture list to:\n {}".format("\n".join([i.format_katcp() for i in self._antennas])))
        reference_antenna = yield self._delay_client.sensor.reference_antenna.get_value()
        self._reference_antenna = Antenna(reference_antenna)
        log.debug("Reference antenna: {}".format(self._reference_antenna.format_katcp()))

    @coroutine
    def start(self):
        """
        @brief   Start the delay buffer controller

        @detail  This method will create all necessary posix shared memory segments
                 and semaphores, retreive necessary information from the delay
                 configuration server and start the delay update callback loop.
        """
        yield self.fetch_config_info()
        self.register_callbacks()
        self.unlink_all()
        # This semaphore is required to protect access to the shared_buffer
        # so that it is not read and written simultaneously
        # The value is set to two such that two processes can read simultaneously
        log.info("Creating mutex semaphore, key='{}'".format(self.mutex_semaphore_key))
        self._mutex_semaphore = posix_ipc.Semaphore(
            self.mutex_semaphore_key,
            flags=posix_ipc.O_CREX,
            initial_value=self._nreaders)

        # This semaphore is used to notify beamformer instances of a change to the
        # delay models. Upon any change its value is simply incremented by one.
        # Note: There sem_getvalue does not work on Mac OS X so the value of this
        # semaphore cannot be tested on OS X (this is only a problem for local testing).
        log.info("Creating counting semaphore, key='{}'".format(self.counting_semaphore_key))
        self._counting_semaphore = posix_ipc.Semaphore(self.counting_semaphore_key,
            flags=posix_ipc.O_CREX,
            initial_value=0)

        # This is the share memory buffer that contains the delay models for the
        log.info("Creating shared memory, key='{}'".format(self.shared_buffer_key))
        self._shared_buffer = posix_ipc.SharedMemory(
            self.shared_buffer_key,
            flags=posix_ipc.O_CREX,
            size=len(self._delays_array.tobytes()))

        # For reference one can access this memory from another python process using:
        # shm = posix_ipc.SharedMemory("delay_buffer")
        # data_map = mmap.mmap(shm.fd, shm.size)
        # data = np.frombuffer(data_map, dtype=[("delay_rate","float32"),("delay_offset","float32")])
        # data = data.reshape(nbeams, nantennas)

        self._shared_buffer_mmap = mmap(self._shared_buffer.fd, self._shared_buffer.size)
        self._update_callback = PeriodicCallback(self._safe_update_delays, self._update_rate*1000)
        self._update_callback.start()

    def stop(self):
        """
        @brief   Stop the delay buffer controller

        @detail  This method will stop the delay update callback loop, deregister
                 any sensor callbacks and trigger the closing and unlinking of
                 posix IPC objects.
        """
        self._update_callback.stop()
        self.deregister_callbacks()
        log.debug("Closing shared memory mmap and file descriptor")
        self._shared_buffer_mmap.close()
        self._shared_buffer.close_fd()
        self.unlink_all()

    def _update_phase_reference(self, rt, t, status, value):
        if status != "nominal":
            return
        log.debug("Received update to phase-reference: {}, {}, {}, {}".format(rt, t, status, value))
        self._phase_reference = Target(value)

    def register_callbacks(self):
        """
        @brief   Register callbacks on the phase-reference and target positions for each beam

        @detail  The delay configuration server provides information about antennas, reference
                 antennas, phase centres and beam targets. It is currently assumed that the
                 antennas and reference antenna will not (can not) change during an observation
                 as such we here only register callbacks on the phase-reference (a KATPOINT target
                 string specifying the bore sight pointing position) and the individial beam targets.
        """
        log.debug("Registering phase-reference update callback")
        self._delay_client.sensor.phase_reference.set_sampling_strategy('event')
        self._delay_client.sensor.phase_reference.register_listener(self._update_phase_reference)
        for beam in self._ordered_beams:
            sensor_name = "{}_target".format(beam)
            def callback(rt, t, status, value, beam):
                log.debug("Received target update for beam {}: {}".format(beam, value))
                if status == 'nominal':
                    try:
                        self._targets[beam] = Target(value)
                    except Exception as error:
                        log.exception("Error when updating target for beam {}".format(beam))
            self._delay_client.sensor[sensor_name].set_sampling_strategy('event')
            self._delay_client.sensor[sensor_name].register_listener(
                lambda rt, r, status, value, beam=beam: callback(rt, r, status, value, beam))
            self._beam_callbacks[beam] = callback

    def deregister_callbacks(self):
        """
        @brief    Deregister any callbacks started with the register callbacks method
        """
        log.debug("Deregistering phase-reference update callback")
        self._delay_client.sensor.phase_reference.set_sampling_strategy('none')
        self._delay_client.sensor.phase_reference.unregister_listener(self._update_phase_reference)
        log.debug("Deregistering targets update callbacks")
        for beam in self._ordered_beams:
            sensor_name = "{}_target".format(beam)
            self._delay_client.sensor[sensor_name].set_sampling_strategy('none')
            self._delay_client.sensor[sensor_name].unregister_listener(self._beam_callbacks[beam])
        self._beam_callbacks = {}

    def _safe_update_delays(self):
        # This is just a wrapper around update delays that
        # stops it throwing an exception
        try:
            self.update_delays()
        except Exception as error:
            log.exception("Failure while updating delays")

    def update_delays(self):
        """
        @brief    Calculate updated delays based on the currently set targets and
                  phase reference.

        @detail   The delays will be calculated in the order specified in the constructor
                  of the class and the delays will be written to the shared memory segment.

                  Two semaphores are used here:
                    - mutex: This is required to stop clients reading the shared
                             memory segment while it is being written to.
                    - counting: This semaphore is incremented after a succesful write
                                to inform clients that there is fresh data to read
                                from the shared memory segment. It is the responsibility
                                of client applications to track the value of this semaphore.
        """
        log.info("Updating delays")
        timer = Timer()
        delay_calc = DelayPolynomial(self._antennas, self._phase_reference,
            self._targets.values(), self._reference_antenna)
        poly = delay_calc.get_delay_polynomials(time.time(), duration=self._delay_span)
        poly_calc_time = timer.elapsed()
        log.debug("Poly calculation took {} seconds".format(poly_calc_time))
        if poly_calc_time >= self._update_rate:
            log.warning("The time required for polynomial calculation >= delay update rate, "
                "this may result in degredation of beamforming quality")
        timer.reset()
        # Acquire the semaphore for each possible reader
        log.debug("Acquiring semaphore for each reader")
        for ii in range(self._nreaders):
            self._mutex_semaphore.acquire()
        self._shared_buffer_mmap.seek(0)
        self._shared_buffer_mmap.write(poly.astype('float32').tobytes())
        # Increment the counting semaphore to notify the readers
        # that a new model is available
        log.debug("Incrementing counting semaphore")
        self._counting_semaphore.release()
        # Release the semaphore for each reader
        log.debug("Releasing semaphore for each reader")
        for ii in range(self._nreaders):
            self._mutex_semaphore.release()
        log.debug("Delay model writing took {} seconds on worker side".format(timer.elapsed()))
    def setup_sensors(self):
        """
        @brief    Setup the default KATCP sensors.

        @note     As this call is made only upon an FBFUSE configure call a mass inform
                  is required to let connected clients know that the proxy interface has
                  changed.
        """
        self._state_sensor = LoggingSensor.discrete(
            "state",
            description = "Denotes the state of this FBF instance",
            params = self.STATES,
            default = self.IDLE,
            initial_status = Sensor.NOMINAL)
        self._state_sensor.set_logger(self.log)
        self.add_sensor(self._state_sensor)

        self._ca_address_sensor = Sensor.string(
            "configuration-authority",
            description = "The address of the server that will be deferred to for configurations",
            default = "",
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._ca_address_sensor)

        self._available_antennas_sensor = Sensor.string(
            "available-antennas",
            description = "The antennas that are currently available for beamforming",
            default = json.dumps({antenna.name:antenna.format_katcp() for antenna in self._katpoint_antennas}),
            initial_status = Sensor.NOMINAL)
        self.add_sensor(self._available_antennas_sensor)

        self._phase_reference_sensor = Sensor.string(
            "phase-reference",
            description="A KATPOINT target string denoting the F-engine phasing centre",
            default="unset,radec,0,0",
            initial_status=Sensor.UNKNOWN)
        self.add_sensor(self._phase_reference_sensor)

        reference_antenna = Antenna("reference,{ref.lat},{ref.lon},{ref.elev}".format(
            ref=self._katpoint_antennas[0].ref_observer))
        self._reference_antenna_sensor = Sensor.string(
            "reference-antenna",
            description="A KATPOINT antenna string denoting the reference antenna",
            default=reference_antenna.format_katcp(),
            initial_status=Sensor.NOMINAL)
        self.add_sensor(self._reference_antenna_sensor)

        self._bandwidth_sensor = Sensor.float(
            "bandwidth",
            description = "The bandwidth this product is configured to process",
            default = self._default_sb_config['bandwidth'],
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._bandwidth_sensor)

        self._nchans_sensor = Sensor.integer(
            "nchannels",
            description = "The number of channels to be processesed",
            default = self._n_channels,
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._nchans_sensor)

        self._cfreq_sensor = Sensor.float(
            "centre-frequency",
            description = "The centre frequency of the band this product configured to process",
            default = self._default_sb_config['centre-frequency'],
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._cfreq_sensor)

        self._cbc_nbeams_sensor = Sensor.integer(
            "coherent-beam-count",
            description = "The number of coherent beams that this FBF instance can currently produce",
            default = self._default_sb_config['coherent-beams-nbeams'],
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._cbc_nbeams_sensor)

        self._cbc_nbeams_per_group = Sensor.integer(
            "coherent-beam-count-per-group",
            description = "The number of coherent beams packed into a multicast group",
            default = 1,
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._cbc_nbeams_per_group)

        self._cbc_ngroups = Sensor.integer(
            "coherent-beam-ngroups",
            description = "The number of multicast groups used for coherent beam transmission",
            default = 1,
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._cbc_ngroups)

        self._cbc_nbeams_per_server_set = Sensor.integer(
            "coherent-beam-nbeams-per-server-set",
            description = "The number of beams produced by each server set",
            default = 1,
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._cbc_nbeams_per_server_set)

        self._cbc_tscrunch_sensor = Sensor.integer(
            "coherent-beam-tscrunch",
            description = "The number time samples that will be integrated when producing coherent beams",
            default = self._default_sb_config['coherent-beams-tscrunch'],
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._cbc_tscrunch_sensor)

        self._cbc_fscrunch_sensor = Sensor.integer(
            "coherent-beam-fscrunch",
            description = "The number frequency channels that will be integrated when producing coherent beams",
            default = self._default_sb_config['coherent-beams-fscrunch'],
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._cbc_fscrunch_sensor)

        self._cbc_antennas_sensor = Sensor.string(
            "coherent-beam-antennas",
            description = "The antennas that will be used when producing coherent beams",
            default = self._default_sb_config['coherent-beams-antennas'],
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._cbc_antennas_sensor)

        self._cbc_mcast_groups_sensor = Sensor.string(
            "coherent-beam-multicast-groups",
            description = "Multicast groups used by this instance for sending coherent beam data",
            default = "",
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._cbc_mcast_groups_sensor)

        self._cbc_mcast_groups_mapping_sensor = Sensor.string(
            "coherent-beam-multicast-group-mapping",
            description = "Mapping of mutlicast group address to the coherent beams in that group",
            default= "",
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._cbc_mcast_groups_mapping_sensor)

        self._ibc_nbeams_sensor = Sensor.integer(
            "incoherent-beam-count",
            description = "The number of incoherent beams that this FBF instance can currently produce",
            default = 1,
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._ibc_nbeams_sensor)

        self._ibc_tscrunch_sensor = Sensor.integer(
            "incoherent-beam-tscrunch",
            description = "The number time samples that will be integrated when producing incoherent beams",
            default = self._default_sb_config['incoherent-beam-tscrunch'],
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._ibc_tscrunch_sensor)

        self._ibc_fscrunch_sensor = Sensor.integer(
            "incoherent-beam-fscrunch",
            description = "The number frequency channels that will be integrated when producing incoherent beams",
            default = self._default_sb_config['incoherent-beam-fscrunch'],
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._ibc_fscrunch_sensor)

        self._ibc_antennas_sensor = Sensor.string(
            "incoherent-beam-antennas",
            description = "The antennas that will be used when producing incoherent beams",
            default = self._default_sb_config['incoherent-beam-antennas'],
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._ibc_antennas_sensor)

        self._ibc_mcast_group_sensor = Sensor.string(
            "incoherent-beam-multicast-group",
            description = "Multicast group used by this instance for sending incoherent beam data",
            default = "",
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._ibc_mcast_group_sensor)

        self._servers_sensor = Sensor.string(
            "servers",
            description = "The worker server instances currently allocated to this product",
            default = ",".join(["{s.hostname}:{s.port}".format(s=server) for server in self._servers]),
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._servers_sensor)

        self._nserver_sets_sensor = Sensor.integer(
            "nserver-sets",
            description = "The number of server sets (independent subscriptions to the F-engines)",
            default = 1,
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._nserver_sets_sensor)

        self._nservers_per_set_sensor = Sensor.integer(
            "nservers-per-set",
            description = "The number of servers per server set",
            default = 1,
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._nservers_per_set_sensor)

        self._delay_config_server_sensor = Sensor.string(
            "delay-config-server",
            description = "The address of the delay configuration server for this product",
            default = "",
            initial_status = Sensor.UNKNOWN)
        self.add_sensor(self._delay_config_server_sensor)