예제 #1
0
class AutosavedSimpleIOC(PVGroup):
    """
    An IOC with three uncoupled read/writable PVs

    Scalar PVs
    ----------
    A (int)
    B (float)

    Vectors PVs
    -----------
    C (vector of int)
    """
    autosave_helper = SubGroup(AutosaveHelper)

    A = autosaved(pvproperty(value=1, record='ao'))
    B = pvproperty(value=2.0)
    C = autosaved(pvproperty(value=[1, 2, 3]))

    subgroup = SubGroup(AutosavedSubgroup)
예제 #2
0
class SystemGroupBase(PVGroup):
    """
    PV group for attenuator system-spanning information.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # TODO: this could be done by wrapping SystemGroup
        for obj in (self.best_config, self.active_config, self.filter_moving):
            util.hack_max_length_of_channeldata(obj,
                                                [0] * self.parent.num_filters)

        # TODO: caproto does not make this easy. We explicitly will be using
        # asyncio here.
        self.async_lib = AsyncioAsyncLayer()
        self._context = {}
        self._pv_put_queue = None
        self._put_thread = None

    calculated_transmission = pvproperty(
        value=0.1,
        name='T_CALC',
        record='ao',
        lower_ctrl_limit=0.0,
        upper_ctrl_limit=1.0,
        read_only=True,
        doc='Calculated transmission (all blades)',
        precision=3,
    )

    calculated_transmission_3omega = pvproperty(
        name='T_3OMEGA',
        value=0.5,
        lower_ctrl_limit=0.0,
        upper_ctrl_limit=1.0,
        read_only=True,
        doc='Calculated 3omega transmission (all blades)',
        precision=3,
    )

    best_config_error = pvproperty(
        value=0.1,
        name='BestConfigError_RBV',
        record='ao',
        lower_ctrl_limit=-1.0,
        upper_ctrl_limit=1.0,
        read_only=True,
        doc='Calculated transmission error',
        precision=3,
    )

    moving = pvproperty(name='Moving_RBV',
                        value='False',
                        record='bo',
                        enum_strings=['False', 'True'],
                        read_only=True,
                        doc='Moving to a new configuration.',
                        dtype=ChannelType.ENUM)

    mirror_in = pvproperty(value='False',
                           name='MIRROR_IN',
                           record='bo',
                           enum_strings=['False', 'True'],
                           read_only=True,
                           doc='The inspection mirror is in',
                           dtype=ChannelType.ENUM)

    calc_mode = pvproperty(
        value='Floor',
        name='CalcMode',
        record='bo',
        enum_strings=['Floor', 'Ceiling'],
        read_only=False,
        doc='Mode for selecting floor or ceiling transmission estimation',
        dtype=ChannelType.ENUM)

    energy_source = pvproperty(
        value='Actual',
        name='EnergySource',
        record='bo',
        enum_strings=['Actual', 'Custom'],
        read_only=False,
        doc='Choose the source of photon energy',
        dtype=ChannelType.ENUM,
    )

    best_config = pvproperty(
        name='BestConfiguration_RBV',
        value=0,
        max_length=1,
        read_only=True,
        doc='Best configuration as an array (1 if inserted)',
    )

    best_config_bitmask = pvproperty(
        name='BestConfigurationBitmask_RBV',
        value=0,
        read_only=True,
        doc='Best configuration as an integer',
    )

    active_config = pvproperty(
        name='ActiveConfiguration_RBV',
        value=0,
        max_length=1,
        read_only=True,
        alarm_group='motors',
        doc='Active configuration array',
    )

    active_config_bitmask = pvproperty(
        name='ActiveConfigurationBitmask_RBV',
        value=0,
        read_only=True,
        alarm_group='motors',
        doc='Active configuration represented as an integer',
    )

    filter_moving = pvproperty(
        name='FiltersMoving_RBV',
        value=0,
        max_length=1,
        read_only=True,
        alarm_group='motors',
        doc='Filter motion status as an array (1 if moving)',
    )

    filter_moving_bitmask = pvproperty(
        name='FiltersMovingBitmask_RBV',
        value=0,
        read_only=True,
        alarm_group='motors',
        doc='Filter motion status as an integer',
    )

    energy_actual = pvproperty(
        name='ActualPhotonEnergy_RBV',
        value=0.0,
        read_only=True,
        units='eV',
        alarm_group='valid_photon_energy',
        precision=1,
    )

    transmission_actual = pvproperty(
        name='ActualTransmission_RBV',
        value=0.0,
        read_only=True,
        alarm_group='motors',
        precision=3,
    )

    transmission_3omega_actual = pvproperty(
        name='Actual3OmegaTransmission_RBV',
        value=0.0,
        read_only=True,
        alarm_group='motors',
        precision=3,
    )

    energy_custom = pvproperty(
        name='CustomPhotonEnergy',
        value=0.0,
        read_only=False,
        units='eV',
        lower_ctrl_limit=100.0,
        upper_ctrl_limit=30000.0,
        precision=1,
    )

    last_energy = pvproperty(
        name='LastPhotonEnergy_RBV',
        value=0.0,
        read_only=True,
        units='eV',
        doc='Energy that was used for the calculation.',
        precision=1,
    )

    last_transmission = pvproperty(
        name='LastTransmission_RBV',
        value=0.0,
        lower_ctrl_limit=0.0,
        upper_ctrl_limit=1.0,
        doc='Last desired transmission value',
        precision=3,
        read_only=True,
    )

    last_mode = pvproperty(
        value='Floor',
        name='LastCalcMode_RBV',
        record='bo',
        enum_strings=['Floor', 'Ceiling'],
        read_only=True,
        doc='Last calculation mode',
        dtype=ChannelType.ENUM,
    )

    apply_config = pvproperty(
        name='ApplyConfiguration',
        value='False',
        record='bo',
        enum_strings=['False', 'True'],
        doc='Apply the calculated configuration.',
        dtype=ChannelType.ENUM,
        alarm_group='motors',
    )

    cancel_apply = pvproperty(
        name='Cancel',
        value='False',
        record='bo',
        enum_strings=['False', 'True'],
        doc='Stop trying to apply the configuration.',
        dtype=ChannelType.ENUM,
    )

    desired_transmission = autosaved(
        pvproperty(
            name='DesiredTransmission',
            value=0.5,
            lower_ctrl_limit=0.0,
            upper_ctrl_limit=1.0,
            doc='Desired transmission value',
            precision=3,
        ))

    run = pvproperty(value='False',
                     name='Run',
                     record='bo',
                     enum_strings=['False', 'True'],
                     doc='Run calculation',
                     dtype=ChannelType.ENUM)

    @active_config.startup
    async def active_config(self, instance, async_lib):
        motor_pvnames = self.parent.monitor_pvnames['motors']
        monitor_list = sum((pvlist for pvlist in motor_pvnames.values()), [])
        all_status = {pv: False for pv in monitor_list}

        async def update_connection_status(pv, status):
            all_status[pv] = (status == 'connected')
            await util.alarm_if(instance, not all(all_status.values()),
                                AlarmStatus.LINK)

        async for event, context, data in monitor_pvs(*monitor_list,
                                                      async_lib=async_lib):
            if event == 'connection':
                await update_connection_status(context.name, data)
                continue

            pvname = context.pv.name
            if pvname not in motor_pvnames['get']:
                continue

            idx = self.parent.monitor_pvnames['motors']['get'].index(pvname)
            await self.motor_has_moved(self.parent.first_filter + idx,
                                       data.data[0])

    @energy_actual.startup
    async def energy_actual(self, instance, async_lib):
        """Update beam energy and calculated values."""
        async def update_connection_status(status):
            await util.alarm_if(instance, status != 'connected',
                                AlarmStatus.LINK)

        await update_connection_status('disconnected')
        pvname = self.parent.monitor_pvnames['ev']
        async for event, context, data in monitor_pvs(pvname,
                                                      async_lib=async_lib):
            if event == 'connection':
                self.log.info('%s %s', context, data)
                await update_connection_status(data)
                continue

            eV = data.data[0]
            self.log.debug('Photon energy changed: %s', eV)

            if instance.value != eV:
                delta = instance.value - eV
                if abs(delta) > 1000:
                    self.log.info("Photon energy changed to %s eV.", eV)
                await instance.write(eV)

        return eV

    @apply_config.putter
    async def apply_config(self, instance, value):
        if value == 'False':
            return

        await self.move_blades()

    # apply_config.PROC -> apply_config = 1
    util.process_writes_value(apply_config, value=1)

    @apply_config.startup
    async def apply_config(self, instance, async_lib):
        def put_thread():
            while True:
                pv, value = self._pv_put_queue.get()
                try:
                    pv.write([value], wait=False)
                except Exception:
                    self.log.exception('Failed to put value: %s=%s', pv, value)

        ctx = util.get_default_thread_context()

        self._set_pvs = ctx.get_pvs(
            *self.parent.monitor_pvnames['motors']['set'], timeout=None)

        self._pv_put_queue = self.async_lib.ThreadsafeQueue()
        self._put_thread = threading.Thread(target=put_thread, daemon=True)
        self._put_thread.start()

    async def move_blades(self, *, timeout_threshold=30.0):
        """Move to the calculated configuration."""
        t0 = time.monotonic()

        def check_done():
            elapsed = time.monotonic() - t0
            done = (tuple(self.active_config.value) == tuple(
                self.best_config.value))
            moving = any(status for status in self.filter_moving.value)
            return any((
                done and not moving,
                self.cancel_apply.value == 'True',
                elapsed > timeout_threshold,
            ))

        await self.moving.write(1)

        state = {}
        while not check_done():
            if not await self.move_blade_step(state):
                break

            await self.async_lib.library.sleep(0.1)

        await self.moving.write(0)

    async def _run_calculation_outer(self):
        """
        Setup calculation based on current settings, call run_calculation,
        and then update results based on that.

        Any exception raised here will be caught in the 'run' put handler.
        """
        energy = {
            'Actual': self.energy_actual.value,
            'Custom': self.energy_custom.value,
        }[self.energy_source.value]

        desired_transmission = self.desired_transmission.value
        calc_mode = self.calc_mode.value
        config = await self.run_calculation(
            energy,
            desired_transmission=self.desired_transmission.value,
            calc_mode=calc_mode,
        )

        await self.last_energy.write(energy)
        await self.last_mode.write(calc_mode)
        await self.last_transmission.write(desired_transmission)
        await self.best_config.write(
            [int(state) for state in config.filter_states])
        await self.best_config_bitmask.write(
            util.int_array_to_bit_string(
                [state.is_inserted for state in config.filter_states]))
        await self.best_config_error.write(config.transmission -
                                           desired_transmission)

        await self.calculated_transmission.write(config.transmission)
        # TODO
        # await self.calculated_transmission_3omega.write(
        #     self.calculate_transmission_3omega()
        # )

        self.log.info(
            'Energy %s eV with desired transmission %.2g estimated %.2g '
            '(delta %.3g) configuration: %s',
            energy,
            desired_transmission,
            config.transmission,
            self.best_config_error.value,
            self.best_config.value,
        )

    @run.putter
    async def run(self, instance, value):
        if value == 'False':
            return

        try:
            await self._run_calculation_outer()
        except util.MisconfigurationError as ex:
            self.log.warning('Misconfiguration blocks calculation: %s', ex)
            await self.desired_transmission.alarm.write(
                status=AlarmStatus.CALC,
                severity=AlarmSeverity.MAJOR_ALARM,
            )
        except Exception:
            self.log.exception('Calculation failed unexpectedly')
            await self.desired_transmission.alarm.write(
                status=AlarmStatus.CALC,
                severity=AlarmSeverity.MAJOR_ALARM,
            )

    # RUN.PROC -> run = 1
    util.process_writes_value(run, value=1)

    async def _update_active_transmission(self):
        """Re-calculate transmission_actual based on working filters."""
        config = tuple(self.active_config.value)
        offset = self.parent.first_filter

        transm = np.zeros_like(config) * np.nan
        transm3 = np.zeros_like(config) * np.nan
        for idx, filt in self.active_filters.items():
            zero_index = idx - offset
            if State(config[zero_index]).is_inserted:
                transm[zero_index] = filt.transmission.value
                transm3[zero_index] = filt.transmission_3omega.value

        await self.transmission_actual.write(np.nanprod(transm))
        await self.transmission_3omega_actual.write(np.nanprod(transm3))

    async def move_blade_step(self, state: Dict[int, State]):
        """
        Caller is requesting to move blades in or out.

        The caller is expected to handle timeout scenarios and provide a
        dictionary with which we can record this implementation's state.

        Parameters
        ----------
        state : dict
            State dictionary, which we use here to mark each time we request
            a motion.  This will be passed in on subsequent calls.

        Returns
        -------
        continue_ : bool
            Returns `True` if there are more blades to move.
        """
        items = [(pv, State(active), State(best)) for pv, active, best in zip(
            self._set_pvs, self.active_config.value, self.best_config.value)]

        move_out = {
            pv: best
            for pv, active, best in items
            if not best.is_inserted and active != best
        }
        move_in = {
            pv: best
            for pv, active, best in items
            if best.is_inserted and active != best
        }

        if move_in:
            to_move = move_in
            # Move blades IN first, to be safe
        else:
            to_move = move_out

        for pv, target in to_move.items():
            if state.get(pv, None) != target:
                state[pv] = target
                self.log.debug('Moving %s to %s', pv, target)
                await self._pv_put_queue.async_put((pv, target))

        return bool(move_in or move_out)

    def calculate_transmission(self):
        """Total transmission through all filter blades."""
        t = 1.0
        for filt in self.active_filters.values():
            t *= filt.transmission.value
        return t

    def calculate_transmission_3omega(self):
        """Total 3rd harmonic transmission through all filter blades."""
        t = 1.0
        for filt in self.active_filters.values():
            t *= filt.transmission_3omega.value
        return t

    def get_filters(self, stuck=False, inactive=False, normal=True):
        """
        Get filters matching the specified criteria, sorted by index.

        Parameters
        ----------
        stuck : bool, optional
            Include stuck filters.  Defaults to False.

        inactive : bool, optional
            Include inactive filters.  Defaults to False.

        normal : bool, optional
            Include non-stuck, active filters.  Defaults to True.

        Yields
        ------
        filter : PVGroup
            A matching filter.
        """
        def matches(idx, filt):
            if filt.active.value == "False":
                # Include inactive filters, if requested
                return inactive
            if filt.is_stuck.value != 'Not stuck':
                # Include stuck filters, if requested
                return stuck
            # Include normal filters, if requested
            return normal

        return [
            filt for idx, filt in self.filters.items() if matches(idx, filt)
        ]

    @property
    def first_filter(self):
        """The first filter index in the system."""
        # Indirection for where it's actually stored - in the parent.
        return self.parent.first_filter

    @property
    def filters(self):
        """All filters in the system."""
        # Indirection for where they're actually stored - in the parent.
        return self.parent.filters

    @property
    def stuck_filters(self) -> Dict[int, PVGroup]:
        """A dictionary of all filters that are stuck in a particular state."""
        return {
            idx: filt
            for idx, filt in self.active_filters.items()
            if filt.is_stuck.value != 'Not stuck'
        }

    @property
    def active_filters(self) -> Dict[int, PVGroup]:
        """A dictionary of all filters that are marked as inactive."""
        return {
            idx: filt
            for idx, filt in self.filters.items()
            if filt.active.value == "True"
        }

    def calculate_stuck_transmission(self) -> float:
        """The effective normalized transmission of all stuck filters."""
        transmission = 1.0
        for filt in self.get_filters(stuck=True, inactive=False, normal=False):
            transmission *= filt.transmission.value
        return transmission

    @property
    def all_filter_materials(self) -> List[str]:
        """All filter materials in a list."""
        return [flt.material.value for flt in self.active_filters.values()]

    async def motor_has_moved(self, blade_index, raw_state):
        """
        Callback indicating a motor has moved.

        Update the current configuration, if necessary.

        Parameters
        ----------
        blade_index : int
            Blade index (not zero-based).

        raw_state : int
            Raw state value from control system.
        """
        array_idx = blade_index - self.parent.first_filter
        state = State(int(raw_state))

        flt = self.parent.filters[blade_index]
        await flt.set_inserted_filter_state(state)

        new_config = list(self.active_config.value)
        new_config[array_idx] = int(state)
        if tuple(new_config) != tuple(self.active_config.value):
            self.log.info('Active config changed: %s', new_config)
            await self.active_config.write(new_config)
            await self.active_config_bitmask.write(
                util.int_array_to_bit_string(
                    [State(blade).is_inserted for blade in new_config]))
            await self._update_active_transmission()

        moving = list(self.filter_moving.value)
        moving[array_idx] = state.is_moving
        if tuple(moving) != tuple(self.filter_moving.value):
            await self.filter_moving.write(moving)
            await self.filter_moving_bitmask.write(
                util.int_array_to_bit_string(moving))
예제 #3
0
class AutosavedSubgroup(PVGroup):
    A = autosaved(pvproperty(value=1, record='ao'))
    B = pvproperty(value=2.0)
    C = autosaved(pvproperty(value=[1, 2, 3]))
예제 #4
0
class FilterGroup(PVGroup):
    """
    PVGroup for a single filter - with a specific material and thickness.

    Parameters
    ----------
    prefix : str
        PV prefix.

    index : int
        Index of the filter in the system.
    """
    def __init__(self, prefix, index, **kwargs):
        super().__init__(prefix, **kwargs)
        self._last_photon_energy = 0.0
        self.index = index
        # Default to silicon, for now
        self.load_data('Si')

    def __repr__(self):
        return (f'<{self.__class__.__name__} '
                f'({self.index}) '
                f'{self.material.value} '
                f'{self.thickness.value} um '
                f'T={self.transmission.value}'
                f'>')

    def load_data(self, formula):
        """
        Load the HDF5 dataset containing physical constants
        and photon energy : atomic scattering factor table.
        """
        try:
            self.table = calculator.get_absorption_table(formula=formula)
        except Exception:
            self.table = None
            self.log.exception("Failed to load absorption table for %s",
                               formula)
        else:
            self.log.info("Loaded absorption table for %s", formula)

    material = autosaved(
        pvproperty(value='Si',
                   name='Material',
                   record='stringin',
                   doc='Filter material',
                   dtype=ChannelType.STRING))

    thickness = autosaved(
        pvproperty(
            value=10.,
            name='Thickness',
            record='ao',
            lower_ctrl_limit=0.0,
            upper_ctrl_limit=900000.0,
            doc='Filter thickness',
            units='um',
            precision=1,
        ))

    closest_energy = pvproperty(
        value=0.0,
        name='ClosestEnergy_RBV',
        read_only=True,
        precision=1,
    )

    closest_index = pvproperty(
        name='ClosestIndex_RBV',
        read_only=True,
    )

    transmission = pvproperty(
        name='Transmission_RBV',
        value=0.5,
        upper_ctrl_limit=1.0,
        lower_ctrl_limit=0.0,
        read_only=True,
        precision=3,
    )

    transmission_3omega = pvproperty(
        name='Transmission3Omega_RBV',
        value=0.5,
        upper_alarm_limit=1.0,
        lower_alarm_limit=0.0,
        read_only=True,
        precision=3,
    )

    # What does it mean to be inactive but stuck?
    # stuck,     inactive -> ignore entirely
    # not stuck, inactive -> ignore entirely
    # stuck,       active -> stuck, include in transmission
    # not stuck,   active -> use in calculations
    active = autosaved(
        pvproperty(
            value='True',
            name='Active',
            record='bo',
            enum_strings=['False', 'True'],
            doc='Filter should be used in calculations',
            dtype=ChannelType.ENUM,
        ))

    # TODO: intention is to say it's stuck in/out/etc depending on the state
    # better name would be "StuckAtState"
    # NOTE: this is a PV API change for AT2L0, but it's not currently necessary
    # as of the time of writing, fortunately.
    is_stuck = autosaved(
        pvproperty(
            value='Not stuck',
            name='IsStuck',
            record='mbbo',
            doc='Stuck at indicated state',
            enum_strings=[
                'Not stuck', 'Out', 'In_01', 'In_02', 'In_03', 'In_04',
                'In_05', 'In_06', 'In_07', 'In_08', 'In_09'
            ],
            dtype=ChannelType.ENUM,
        ))

    def get_stuck_state(self) -> State:
        """If marked as stuck, get the stuck State."""
        return State(self.is_stuck.enum_strings.index(self.is_stuck.value))

    async def set_photon_energy(self, energy_ev):
        """
        Set the current photon energy to determine transmission.

        Parameters
        ----------
        energy_ev : float
            The photon energy [eV].
        """
        self._last_photon_energy = energy_ev
        closest_energy, i = calculator.find_closest_energy(
            energy_ev, self.table)

        await self.closest_index.write(i)
        await self.closest_energy.write(closest_energy)
        await self.transmission.write(self.get_transmission(energy_ev))
        await self.transmission_3omega.write(
            self.get_transmission(3. * energy_ev))

    def get_transmission(self, photon_energy_ev: float):
        """
        Get the transmission for the given photon energy based on the material
        and thickness configured.

        Parameters
        ----------
        energy_ev : float
            The photon energy [eV].

        Returns
        -------
        transmission : float
            Normalized transmission value.
        """
        return calculator.get_transmission(
            photon_energy=photon_energy_ev,
            table=self.table,
            thickness=self.thickness.value * 1e-6,  # um -> meters
        )

    @material.putter
    async def material(self, instance, value):
        """
        Update the material - load the table and update transmission values.
        """
        self.load_data(formula=value)
        if (self.table is not None and self.thickness.value > 0.0
                and self._last_photon_energy > 0.0):
            await self.set_photon_energy(self._last_photon_energy)

    @thickness.putter
    async def thickness(self, instance, value):
        """
        Update the thickness
        """
        energy = self._last_photon_energy
        await self.thickness.write(value, verify_value=False)
        await self.transmission.write(self.get_transmission(energy))