Пример #1
0
    def __init__(self, name, role, axes, ranges=None, **kwargs):
        """
        axes (set of string): names of the axes
        ranges (dict string -> float,float): min/max of the axis
        """
        assert len(axes) > 0
        if ranges is None:
            ranges = {}

        axes_def = {}
        self._position = {}
        init_speed = {}
        for a in axes:
            rng = ranges.get(a, (-0.1, 0.1))
            axes_def[a] = model.Axis(unit="m", range=rng, speed=(0., 10.))
            # start at the centre
            self._position[a] = (rng[0] + rng[1]) / 2
            init_speed[a] = 1.0  # we are fast!

        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)

        # Special file "stage.fail" => will cause simulation of hardware error
        if os.path.exists("stage.fail"):
            raise HwError("stage.fail file present, simulating error")

        self._executor = model.CancellableThreadPoolExecutor(max_workers=1)

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()

        self.speed = model.MultiSpeedVA(init_speed, (0., 10.), "m/s")
Пример #2
0
    def __init__(self, name, role, axes, ranges=None, **kwargs):
        """
        axes (set of string): names of the axes
        """
        assert len(axes) > 0
        if ranges is None:
            ranges = {}

        axes_def = {}
        self._position = {}
        init_speed = {}
        for a in axes:
            rng = ranges.get(a, [-0.1, 0.1])
            axes_def[a] = model.Axis(unit="m", range=rng, speed=[0., 10.])
            # start at the centre
            self._position[a] = (rng[0] + rng[1]) / 2
            init_speed[a] = 10.0  # we are super fast!

        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(self._applyInversionAbs(
            self._position),
                                                unit="m",
                                                readonly=True)

        self.speed = model.MultiSpeedVA(init_speed, [0., 10.], "m/s")
Пример #3
0
    def __init__(self, name, role, port, axes, **kwargs):
        """
        port (string): name of the serial port to connect to the controllers
        axes (dict string=> 2-tuple (0<=int<=15, 0<=int<=1)): the configuration of the network.
         for each axis name the controller address and channel
         Note that even if it's made of several controllers, each controller is 
         _not_ seen as a child from the odemis model point of view.
        """

        ser = PIRedStone.openSerialPort(port)
#        ser = FakePIRedStone.openSerialPort(port) # use FakePIRedStone for testing

        # Not to be mistaken with axes which is a simple public view
        self._axis_to_child = {} # axis name -> (PIRedStone, channel)
        axes_def = {} # axis name -> Axis
        # TODO also a rangesRel : min and max of a step
        position = {}
        speed = {}
        controllers = {} # address => PIRedStone
        for axis, (add, channel) in axes.items():
            if not add in controllers:
                controllers[add] = PIRedStone(ser, add)
#                controllers[add] = FakePIRedStone(ser, add) # use FakePIRedStone for testing
            controller = controllers[add]
            self._axis_to_child[axis] = (controller, channel)

            position[axis] = controller.getPosition(channel)
            # TODO request also the ranges from the arguments?
            # For now we put very large one
            ad = model.Axis(canAbs=False, unit="m", range=(-1, 1),
                            speed=(10e-6, 0.5))
            # Just to make sure it doesn't go too fast
            speed[axis] = 0.1 # m/s
            axes_def[axis] = ad

        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)
        
        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(position, unit="m", readonly=True)

        # min speed = don't be crazy slow. max speed from hardware spec
        self.speed = model.MultiSpeedVA(speed, range=[10e-6, 0.5], unit="m/s",
                                        setter=self._setSpeed)
        self._setSpeed(speed)

        # set HW and SW version
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__,
                                                      driver.getSerialDriver(port))
        hwversions = []
        for axis, (ctrl, channel) in self._axis_to_child.items():
            hwversions.append("'%s': %s" % (axis, ctrl.versionReport()))
        self._hwVersion = ", ".join(hwversions)

        # to acquire before sending anything on the serial port
        self.ser_access = threading.Lock()

        self._action_mgr = ActionManager(self)
        self._action_mgr.start()
Пример #4
0
    def __init__(self, name, role, address, axes, stepsize, sn=None, **kwargs):
        """
        address (str): ip address (use "autoip" to automatically scan and find the
          controller, "fake" for a simulator)
        axes (list of str): names of the axes, from the 1st to the 4th, if present.
          if an axis is not connected, put a "".
        stepsize (list of float): size of a step in m (the smaller, the
          bigger will be a move for a given distance in m)
        sn (str or None): serial number of the device (eg, "11500"). If None, the
          driver will use whichever controller is first found.
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis)
        """
        if not 1 <= len(axes) <= 4:
            raise ValueError(
                "Axes must be a list of 1 to 4 axis names (got %s)" % (axes, ))
        if len(axes) != len(stepsize):
            raise ValueError("Expecting %d stepsize (got %s)" %
                             (len(axes), stepsize))
        self._name_to_axis = {}  # str -> int: name -> axis number
        for i, n in enumerate(axes):
            if n == "":  # skip this non-connected axis
                continue
            self._name_to_axis[n] = i + 1

        for sz in stepsize:
            if sz > 10e-3:  # sz is typically ~1µm, so > 1 cm is very fishy
                raise ValueError("stepsize should be in meter, but got %g" %
                                 (sz, ))
        self._stepsize = stepsize

        self._address = address
        self._sn = sn
        self._accesser = self._openConnection(address, sn)
        self._recover = False

        self._resynchonise()

        if name is None and role is None:  # For scan only
            return

        # Seems to really be the device, so handle connection errors fully
        self._recover = True

        modl, fw, sn = self.GetIdentification()
        if modl != "8742":
            logging.warning("Controller %s is not supported, will try anyway",
                            modl)

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time

        # Let the controller check the actuators are connected
        self.MotorCheck()

        axes_def = {}
        speed = {}
        for n, i in self._name_to_axis.items():
            sz = self._stepsize[i - 1]
            # TODO: allow to pass the range in m in the arguments
            # Position supports ±2³¹, probably not that much in reality, but
            # there is no info.
            rng = [(-2**31) * sz, (2**31 - 1) * sz]

            # Check the actuator is connected
            mt = self.GetMotorType(i)
            if mt in {MT_NONE, MT_UNKNOWN}:
                raise HwError(
                    "Controller failed to detect motor %d, check the "
                    "actuator is connected to the controller" % (i, ))
            max_stp_s = {MT_STANDARD: 2000, MT_TINY: 1750}[mt]
            srng = (0, self._speedToMS(i, max_stp_s))
            speed[n] = self._speedToMS(i, self.GetVelocity(i))

            axes_def[n] = model.Axis(range=rng, speed=srng, unit="m")

        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)

        self._swVersion = "%s (IP connection)" % (odemis.__version__, )
        self._hwVersion = "New Focus %s (firmware %s, S/N %s)" % (modl, fw, sn)

        # Note that the "0" position is just the position at which the
        # controller turned on
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()

        max_speed = max(a.speed[1] for a in axes_def.values())
        self.speed = model.MultiSpeedVA(speed,
                                        range=(0, max_speed),
                                        unit="m/s",
                                        setter=self._setSpeed)
Пример #5
0
class Shamrock(model.Actuator):
    """
    Component representing the spectrograph part of the Andor Shamrock
    spectrometers.
    On Linux, only the SR303i is supported, via the I²C cable connected to the
    iDus. Support only works since SDK 2.97.
    Note: we don't handle changing turret.
    """
    def __init__(self, name, role, device, path=None, parent=None, **kwargs):
        """
        device (0<=int or "fake"): device number
        path (None or string): initialisation path of the Andorcam2 SDK or None
          if independent of a camera. If the path is set, a parent should also
          be passed, which is a DigitalCamera component.
        inverted (None): it is not allowed to invert the axes
        """
        # From the documentation:
        # If controlling the shamrock through i2c it is important that both the
        # camera and spectrograph are being controlled through the same calling
        # program and that the DLLs used are contained in the same working
        # folder. The camera MUST be initialized before attempting to
        # communicate with the Shamrock.
        if kwargs.get("inverted", None):
            raise ValueError("Axis of spectrograph cannot be inverted")

        if device == "fake":
            self._dll = FakeShamrockDLL(parent)
            device = 0
        else:
            self._dll = ShamrockDLL()
        self._path = path or ""
        self._device = device

        try:
            self.Initialize()
        except ShamrockDLL, err:
            raise IOError("Failed to find Andor Shamrock (%s) as device %d" %
                          (name, device))
        try:
            nd = self.GetNumberDevices()
            if device >= nd:
                raise IOError(
                    "Failed to find Andor Shamrock (%s) as device %d" %
                    (name, device))

            if path is None or parent is None:
                raise NotImplementedError(
                    "Shamrock without parent a camera is not implemented")

            ccd = None
            if (parent and hasattr(parent, "_detector")
                    and isinstance(parent._detector, andorcam2.AndorCam2)):
                ccd = parent._detector
            self._hw_access = HwAccessMgr(ccd)

            # for now, it's fixed (and it's unlikely to be useful to allow less than the max)
            max_speed = 1000e-9 / 5  # about 1000 nm takes 5s => max speed in m/s
            self.speed = model.MultiSpeedVA({"wavelength": max_speed},
                                            range=[max_speed, max_speed],
                                            unit="m/s",
                                            readonly=True)

            gchoices = self._getGratingChoices()

            # Find lowest and largest wavelength reachable
            wl_range = (float("inf"), float("-inf"))
            for g in gchoices:
                wmin, wmax = self.GetWavelengthLimits(g)
                wl_range = min(wl_range[0], wmin), max(wl_range[1], wmax)

            # Slit (we only actually care about the input side slit for now)
            slits = {
                "input side": 1,
                "input direct": 2,
                "output side": 3,
                "output direct": 4,
            }
            for slitn, i in slits.items():
                logging.info("Slit %s is %spresent", slitn,
                             "" if self.AutoSlitIsPresent(i) else "not ")

            axes = {
                "wavelength":
                model.Axis(unit="m",
                           range=wl_range,
                           speed=(max_speed, max_speed)),
                "grating":
                model.Axis(choices=gchoices)
            }

            # add slit input direct if available
            # Note: the documentation mentions the width is in mm,
            # but it's probably actually µm (10 is the minimum).
            if self.AutoSlitIsPresent(INPUT_SLIT_SIDE):
                self._slit = INPUT_SLIT_SIDE
                axes["slit"] = model.Axis(
                    unit="m", range=[SLITWIDTHMIN * 1e-6, SLITWIDTHMAX * 1e-6])
            else:
                self._slit = None

            # provides a ._axes
            model.Actuator.__init__(self,
                                    name,
                                    role,
                                    axes=axes,
                                    parent=parent,
                                    **kwargs)

            # set HW and SW version
            self._swVersion = "%s" % (odemis.__version__)
            # TODO: EEPROM contains name of the device, but there doesn't seem to be any function for getting it?!
            self._hwVersion = "%s (s/n: %s)" % ("Andor Shamrock",
                                                self.GetSerialNumber())

            # will take care of executing axis move asynchronously
            self._executor = CancellableThreadPoolExecutor(
                max_workers=1)  # one task at a time

            # RO, as to modify it the client must use .moveRel() or .moveAbs()
            self.position = model.VigilantAttribute({},
                                                    unit="m",
                                                    readonly=True)
            self._updatePosition()

        except Exception:
            self.Close()
            raise
Пример #6
0
    def __init__(self,
                 name,
                 role,
                 port,
                 turret=None,
                 calib=None,
                 _noinit=False,
                 dependencies=None,
                 **kwargs):
        """
        port (string): name of the serial port to connect to.
        turret (None or 1<=int<=3): turret number set-up. If None, consider that
          the current turret known by the device is correct.
        calib (None or list of (int, int and 5 x (float or str))):
          calibration data, as saved by Winspec. Data can be either in float
          or as an hexadecimal value "hex:9a,99,99,99,99,79,40,40"
           blaze in nm, groove gl/mm, center adjust, slope adjust,
           focal length, inclusion angle, detector angle
        inverted (None): it is not allowed to invert the axes
        dependencies (dict str -> Component): "ccd" should be the CCD used to acquire
         the spectrum.
        _noinit (boolean): for internal use only, don't try to initialise the device
        """
        if kwargs.get("inverted", None):
            raise ValueError("Axis of spectrograph cannot be inverted")

        # start with this opening the port: if it fails, we are done
        try:
            self._serial = self.openSerialPort(port)
        except serial.SerialException:
            raise HwError(
                "Failed to find spectrograph %s (on port '%s'). "
                "Check the device is turned on and connected to the "
                "computer. You might need to turn it off and on again." %
                (name, port))
        self._port = port

        # to acquire before sending anything on the serial port
        self._ser_access = threading.Lock()

        self._try_recover = False
        if _noinit:
            return

        self._initDevice()
        self._try_recover = True

        try:
            self._ccd = dependencies["ccd"]
        except (TypeError, KeyError):
            # TODO: only needed if there is calibration info (for the pixel size)
            # otherwise it's fine without CCD.
            raise ValueError("Spectrograph needs a dependency 'ccd'")

        # according to the model determine how many gratings per turret
        model_name = self.GetModel()
        self.max_gratings = MAX_GRATINGS_NUM.get(model_name, 3)

        if turret is not None:
            if turret < 1 or turret > self.max_gratings:
                raise ValueError(
                    "Turret number given is %s, while expected a value between 1 and %d"
                    % (turret, self.max_gratings))
            self.SetTurret(turret)
            self._turret = turret
        else:
            self._turret = self.GetTurret()

        # for now, it's fixed (and it's unlikely to be useful to allow less than the max)
        max_speed = 1000e-9 / 10  # about 1000 nm takes 10s => max speed in m/s
        self.speed = model.MultiSpeedVA(max_speed,
                                        range=[max_speed, max_speed],
                                        unit="m/s",
                                        readonly=True)

        gchoices = self.GetGratingChoices()
        # remove the choices which are not valid for the current turret
        for c in gchoices:
            t = 1 + (c - 1) // self.max_gratings
            if t != self._turret:
                del gchoices[c]

        # TODO: report the grating with its wavelength range (possible to compute from groove density + blaze wl?)
        # range also depends on the max grating angle (40°, CCD pixel size, CCD horizontal size, focal length,+ efficienty curve?)
        # cf http://www.roperscientific.de/gratingcalcmaster.html

        # TODO: a more precise way to find the maximum wavelength (looking at the available gratings?)
        # TODO: what's the min? 200nm seems the actual min working, although wavelength is set to 0 by default !?
        axes = {
            "wavelength":
            model.Axis(unit="m",
                       range=(0, 2400e-9),
                       speed=(max_speed, max_speed)),
            "grating":
            model.Axis(choices=gchoices)
        }
        # provides a ._axes
        model.Actuator.__init__(self,
                                name,
                                role,
                                axes=axes,
                                dependencies=dependencies,
                                **kwargs)

        # First step of parsing calib parmeter: convert to (int, int) -> ...
        calib = calib or ()
        if not isinstance(calib, collections.Iterable):
            raise ValueError("calib parameter must be in the format "
                             "[blz, gl, ca, sa, fl, ia, da], "
                             "but got %s" % (calib, ))
        dcalib = {}
        for c in calib:
            if not isinstance(c, collections.Iterable) or len(c) != 7:
                raise ValueError("calib parameter must be in the format "
                                 "[blz, gl, ca, sa, fl, ia, da], "
                                 "but got %s" % (c, ))
            gt = (c[0], c[1])
            if gt in dcalib:
                raise ValueError(
                    "calib parameter contains twice calibration for "
                    "grating (%d nm, %d gl/mm)" % gt)
            dcalib[gt] = c[2:]

        # store calibration for pixel -> wavelength conversion and wavelength offset
        # int (grating number 1 -> 9) -> center adjust, slope adjust,
        #     focal length, inclusion angle/2, detector angle
        self._calib = {}
        # TODO: read the info from MONO-EESTATUS (but it's so
        # huge that it's not fun to parse). There is also detector angle.
        dfl = FOCAL_LENGTH_OFFICIAL[model_name]  # m
        dia = math.radians(INCLUSION_ANGLE_OFFICIAL[model_name])  # rad
        for i in gchoices:
            # put default values
            self._calib[i] = (0, 0, dfl, dia, 0)
            try:
                blz = self._getBlaze(i)  # m
                gl = self._getGrooveDensity(i)  # gl/m
            except ValueError:
                logging.warning("Failed to parse info of grating %d" % i,
                                exc_info=True)
                continue

            # parse calib info
            gt = (int(blz * 1e9), int(gl * 1e-3))
            if gt in dcalib:
                calgt = dcalib[gt]
                ca = self._readCalibVal(calgt[0])  # ratio
                sa = self._readCalibVal(calgt[1])  # ratio
                fl = self._readCalibVal(calgt[2]) * 1e-3  # mm -> m
                ia = math.radians(self._readCalibVal(calgt[3]))  # ° -> rad
                da = math.radians(self._readCalibVal(calgt[4]))  # ° -> rad
                self._calib[i] = ca, sa, fl, ia, da
                logging.info(
                    "Calibration data for grating %d (%d nm, %d gl/mm) "
                    "-> %s" % (i, gt[0], gt[1], self._calib[i]))
            else:
                logging.warning("No calibration data for grating %d "
                                "(%d nm, %d gl/mm)" % (i, gt[0], gt[1]))

        # set HW and SW version
        self._swVersion = "%s (serial driver: %s)" % (
            odemis.__version__, driver.getSerialDriver(port))
        self._hwVersion = "%s (s/n: %s)" % (model_name,
                                            (self.GetSerialNumber()
                                             or "Unknown"))

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time

        # for storing the latest calibrated wavelength value
        self._wl = (None, None, None
                    )  # grating id, raw center wl, calibrated center wl
        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()
Пример #7
0
    def __init__(self, name, role, children, axes_map, ref_on_init=None, **kwargs):
        """
        name (string)
        role (string)
        children (dict str -> actuator): axis name (in this actuator) -> actuator to be used for this axis
        axes_map (dict str -> str): axis name in this actuator -> axis name in the child actuator
        ref_on_init (None, list or dict (str -> float or None)): axes to be referenced during
          initialization. If it's a dict, it will go the indicated position
          after referencing, otherwise, it'll stay where it is.
        """
        if not children:
            raise ValueError("MultiplexActuator needs children")

        if set(children.keys()) != set(axes_map.keys()):
            raise ValueError("MultiplexActuator needs the same keys in children and axes_map")

        # Convert ref_on_init list to dict with no explicit move after
        if isinstance(ref_on_init, list):
            ref_on_init = {a: None for a in ref_on_init}
        self._ref_on_init = ref_on_init or {}
        self._axis_to_child = {} # axis name => (Actuator, axis name)
        self._position = {}
        self._speed = {}
        self._referenced = {}
        axes = {}

        for axis, child in children.items():
            caxis = axes_map[axis]
            self._axis_to_child[axis] = (child, caxis)

            # Ducktyping (useful to support also testing with MockComponent)
            # At least, it has .axes
            if not isinstance(child, model.ComponentBase):
                raise ValueError("Child %s is not a component." % (child,))
            if not hasattr(child, "axes") or not isinstance(child.axes, dict):
                raise ValueError("Child %s is not an actuator." % child.name)
            axes[axis] = copy.deepcopy(child.axes[caxis])
            self._position[axis] = child.position.value[axes_map[axis]]
            if model.hasVA(child, "speed") and caxis in child.speed.value:
                self._speed[axis] = child.speed.value[caxis]
            if model.hasVA(child, "referenced") and caxis in child.referenced.value:
                self._referenced[axis] = child.referenced.value[caxis]

        # this set ._axes and ._children
        model.Actuator.__init__(self, name, role, axes=axes,
                                children=children, **kwargs)

        if len(self.children.value) > 1:
            # will take care of executing axis move asynchronously
            self._executor = CancellableThreadPoolExecutor(max_workers=1)  # one task at a time
            # TODO: make use of the 'Cancellable' part (for now cancelling a running future doesn't work)
        else:  # Only one child => optimize by passing all requests directly
            self._executor = None

        # keep a reference to the subscribers so that they are not
        # automatically garbage collected
        self._subfun = []

        children_axes = {} # dict actuator -> set of string (our axes)
        for axis, (child, ca) in self._axis_to_child.items():
            logging.debug("adding axis %s to child %s", axis, child.name)
            if child in children_axes:
                children_axes[child].add(axis)
            else:
                children_axes[child] = {axis}

        # position & speed: special VAs combining multiple VAs
        self.position = model.VigilantAttribute(self._position, readonly=True)
        for c, ax in children_axes.items():
            def update_position_per_child(value, ax=ax, c=c):
                logging.debug("updating position of child %s", c.name)
                for a in ax:
                    try:
                        self._position[a] = value[axes_map[a]]
                    except KeyError:
                        logging.error("Child %s is not reporting position of axis %s", c.name, a)
                self._updatePosition()
            c.position.subscribe(update_position_per_child)
            self._subfun.append(update_position_per_child)

        # TODO: change the speed range to a dict of speed ranges
        self.speed = model.MultiSpeedVA(self._speed, [0., 10.], setter=self._setSpeed)
        for axis in self._speed.keys():
            c, ca = self._axis_to_child[axis]
            def update_speed_per_child(value, a=axis, ca=ca, cname=c.name):
                try:
                    self._speed[a] = value[ca]
                except KeyError:
                    logging.error("Child %s is not reporting speed of axis %s (%s): %s", cname, a, ca, value)
                self._updateSpeed()

            c.speed.subscribe(update_speed_per_child)
            self._subfun.append(update_speed_per_child)

        # whether the axes are referenced
        self.referenced = model.VigilantAttribute(self._referenced.copy(), readonly=True)

        for axis in self._referenced.keys():
            c, ca = self._axis_to_child[axis]
            def update_ref_per_child(value, a=axis, ca=ca, cname=c.name):
                try:
                    self._referenced[a] = value[ca]
                except KeyError:
                    logging.error("Child %s is not reporting reference of axis %s (%s)", cname, a, ca)
                self._updateReferenced()

            c.referenced.subscribe(update_ref_per_child)
            self._subfun.append(update_ref_per_child)

        for axis, pos in self._ref_on_init.items():
            # If the axis can be referenced => do it now (and move to a known position)
            if axis not in self._referenced:
                raise ValueError("Axis '%s' cannot be referenced, while should be referenced at init" % (axis,))
            if not self._referenced[axis]:
                # The initialisation will not fail if the referencing fails, but
                # the state of the component will be updated
                def _on_referenced(future, axis=axis):
                    try:
                        future.result()
                    except Exception as e:
                        c, ca = self._axis_to_child[axis]
                        c.stop({ca})  # prevent any move queued
                        self.state._set_value(e, force_write=True)
                        logging.exception(e)

                f = self.reference({axis})
                f.add_done_callback(_on_referenced)

            # If already referenced => directly move
            # otherwise => put move on the queue, so that any move by client will
            # be _after_ the init position.
            if pos is not None:
                self.moveAbs({axis: pos})
Пример #8
0
    def __init__(self, name, role, vas=None, axes=None, **kwargs):
        """
        Only VA's specified in vas are created. Their type is determined based on the supplied initial value, and the
        presence of range or choices in.
        Both the presence of VA's and the presence of axes are optional.

        vas (dict (string -> dict (string -> any))): This dict maps desired VA names to a dict with VA properties. The
        VA property dict can contain the following keys:
            "value" (any): initial values of the VA
            "readonly" (bool): optional, True for read only VA, defaults to False
            "unit" (str or None): optional, the unit of the VA, defaults to None
            "range" (float, float): optional, min/max of the VA. Incompatible with "choices".
            "choices" (list, set or dict): optional, possible values available to the VA.
               Incompatible with "range".
        axes (dict (string -> dict (string -> any))): dict mapping desired axis names to dicts with axis properties. The
        axis property dict can contain the following keys:
            "unit" (str): optional, unit of the axis, defaults to "m"
            "range" (float, float): optional, min/max of the axis, defaults to (-0.1, 0.1)
            "choices" (dict): optional, alternative to ranges, these are the choices of the axis
            "speed" (float, float): optional, allowable range of speeds, defaults to (0., 10.)

        """
        # Create desired VA's
        if vas:
            for vaname, vaprop in vas.items():
                # Guess an appropriate VA type based on the initial value and the presence of range or choices
                try:
                    value = vaprop["value"]
                except AttributeError:
                    # TODO: support "short-cut" by using a choice or range
                    raise AttributeError(
                        f"VA {vaname}, does not have a 'value' key.")

                if "choices" in vaprop:
                    if "range" in vaprop:
                        raise ValueError(
                            f"VA {vaname}, has both a range and choice, only one is possible."
                        )
                    # Always keep it simple as "VAEnumerated", it fits any type.
                    vaclass = model.VAEnumerated
                    # The "choices" argument can be either a dict or a set.
                    # However, YAML, doesn't supports set. So we accept list,
                    # and convert to a set.
                    if isinstance(vaprop["choices"], list):
                        vaprop["choices"] = set(vaprop["choices"])
                elif isinstance(value, str):
                    if "range" in vaprop:
                        raise ValueError("String doesn't support range")
                    vaclass = model.StringVA
                elif isinstance(value, bool):
                    if "range" in vaprop:
                        raise ValueError("Boolean doesn't support range")
                    vaclass = model.BooleanVA
                elif isinstance(value, float):
                    if "range" in vaprop:
                        vaclass = model.FloatContinuous
                    else:
                        vaclass = model.FloatVA
                elif isinstance(value, int):
                    if "range" in vaprop:
                        vaclass = model.IntContinuous
                    else:
                        vaclass = model.IntVA
                elif isinstance(value, Iterable):
                    # It's a little tricky because YAML only supports lists.
                    # So we guess a ListVA for the basic type (which is the most full-feature),
                    # and if there is a range, use TupleContinuous, as List doesn't
                    # support a range.
                    if "range" in vaprop:
                        vaclass = model.TupleContinuous
                    else:
                        vaclass = model.ListVA
                else:
                    raise ValueError(
                        f"VA {vaname}, has unsupported value type {value.__class__.__name__}."
                    )

                va = vaclass(**vaprop)
                setattr(self, vaname, va)

        # Create desired axes
        axes_def = {}
        if axes:
            self._position = {}
            init_speed = {}
            for axisname, axisprop in axes.items():
                init_speed[axisname] = 1.0  # we are fast!
                if "range" not in axisprop and "choices" not in axisprop:  # if no range nor choices are defined
                    axisprop["range"] = (-0.1, 0.1)  # use the default range
                if "speed" not in axisprop:
                    axisprop["speed"] = (0., 10.)  # default speed
                axes_def[axisname] = model.Axis(**axisprop)
                if "range" in axisprop:
                    self._position[axisname] = (
                        axisprop["range"][0] +
                        axisprop["range"][1]) / 2  # start at the centre
                else:
                    self._position[axisname] = next(iter(
                        axisprop["choices"]))  # start at an arbitrary value

            self._executor = model.CancellableThreadPoolExecutor(max_workers=1)

            # RO, as to modify it the client must use .moveRel() or .moveAbs()
            self.position = model.VigilantAttribute({},
                                                    unit="m",
                                                    readonly=True)

            self.speed = model.MultiSpeedVA(init_speed, (0., 10.), "m/s")

        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)

        if hasattr(self, "position"):
            self._updatePosition()
Пример #9
0
    def __init__(self, name, role, port, turret=None, _noinit=False, **kwargs):
        """
        port (string): name of the serial port to connect to.
        turret (None or 1<=int<=3): turret number set-up. If None, consider that
          the current turret known by the device is correct.
        inverted (None): it is not allowed to invert the axes
        _noinit (boolean): for internal use only, don't try to initialise the device
        """
        if kwargs.get("inverted", None):
            raise ValueError("Axis of spectrograph cannot be inverted")

        # start with this opening the port: if it fails, we are done
        try:
            self._serial = self.openSerialPort(port)
        except serial.SerialException:
            raise HwError("Failed to find spectrograph %s (on port '%s'). "
                          "Check the device is turned on and connected to the "
                          "computer. You might need to turn it off and on again."
                          % (name, port))
        self._port = port

        # to acquire before sending anything on the serial port
        self._ser_access = threading.Lock()

        self._try_recover = False
        if _noinit:
            return

        self._initDevice()
        self._try_recover = True

        # according to the model determine how many gratings per turret
        model_name = self.GetModel()
        self.max_gratings = MAX_GRATINGS_NUM.get(model_name, 3)

        if turret is not None:
            if turret < 1 or turret > self.max_gratings:
                raise ValueError("Turret number given is %s, while expected a value between 1 and %d" %
                                 (turret, self.max_gratings))
            self.SetTurret(turret)
            self._turret = turret
        else:
            self._turret = self.GetTurret()

        # for now, it's fixed (and it's unlikely to be useful to allow less than the max)
        max_speed = 1000e-9 / 10 # about 1000 nm takes 10s => max speed in m/s
        self.speed = model.MultiSpeedVA(max_speed, range=[max_speed, max_speed], unit="m/s",
                                        readonly=True)

        gchoices = self.GetGratingChoices()
        # remove the choices which are not valid for the current turret
        for c in gchoices:
            t = 1 + (c - 1) // self.max_gratings
            if t != self._turret:
                del gchoices[c]

        # TODO: report the grating with its wavelength range (possible to compute from groove density + blaze wl?)
        # range also depends on the max grating angle (40°, CCD pixel size, CCD horizontal size, focal length,+ efficienty curve?)
        # cf http://www.roperscientific.de/gratingcalcmaster.html

        # TODO: a more precise way to find the maximum wavelength (looking at the available gratings?)
        # TODO: what's the min? 200nm seems the actual min working, although wavelength is set to 0 by default !?
        axes = {"wavelength": model.Axis(unit="m", range=(0, 2400e-9),
                                         speed=(max_speed, max_speed)),
                "grating": model.Axis(choices=gchoices)
                }
        # provides a ._axes
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)

        # set HW and SW version
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver.getSerialDriver(port))
        self._hwVersion = "%s (s/n: %s)" % (model_name, (self.GetSerialNumber() or "Unknown"))

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(max_workers=1) # one task at a time

        pos = {"wavelength": self.GetWavelength(),
               "grating": self.GetGrating()}
        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(pos, unit="m", readonly=True)

        # store focal length and inclusion angle for the polynomial computation
        try:
            self._focal_length = FOCAL_LENGTH_OFFICIAL[model_name]
            self._inclusion_angle = math.radians(INCLUSION_ANGLE_OFFICIAL[model_name])
        except KeyError:
            self._focal_length = None
            self._inclusion_angle = None
Пример #10
0
    def __init__(self, name, role, children, axes_map, **kwargs):
        """
        name (string)
        role (string)
        children (dict str -> actuator): axis name -> actuator to be used for this axis
        axes_map (dict str -> str): axis name in this actuator -> axis name in the child actuator
        """
        if not children:
            raise ValueError("MultiplexActuator needs children")

        if set(children.keys()) != set(axes_map.keys()):
            raise ValueError(
                "MultiplexActuator needs the same keys in children and axes_map"
            )

        self._axis_to_child = {}  # axis name => (Actuator, axis name)
        self._position = {}
        self._speed = {}
        self._referenced = {}
        axes = {}
        for axis, child in children.items():
            # self._children.add(child)
            child.parent = self
            self._axis_to_child[axis] = (child, axes_map[axis])

            # Ducktyping (useful to support also testing with MockComponent)
            # At least, it has .axes
            if not isinstance(child, model.ComponentBase):
                raise ValueError("Child %s is not a component." % str(child))
            if not hasattr(child, "axes") or not isinstance(child.axes, dict):
                raise ValueError("Child %s is not an actuator." % str(child))
            axes[axis] = child.axes[axes_map[axis]]
            self._position[axis] = child.position.value[axes_map[axis]]
            if (hasattr(child, "speed")
                    and isinstance(child.speed, model.VigilantAttributeBase)):
                self._speed[axis] = child.speed.value[axes_map[axis]]
            if (hasattr(child, "referenced") and isinstance(
                    child.referenced, model.VigilantAttributeBase)):
                try:
                    self._referenced[axis] = child.referenced.value[
                        axes_map[axis]]
                except KeyError:
                    pass  # the axis is not referencable => fine

        # TODO: test/finish conversion to Axis
        # this set ._axes and ._children
        model.Actuator.__init__(self,
                                name,
                                role,
                                axes=axes,
                                children=children,
                                **kwargs)

        # keep a reference to the subscribers so that they are not
        # automatically garbage collected
        self._subfun = []

        children_axes = {}  # dict actuator -> set of string (our axes)
        for axis, (child, axis_mapped) in self._axis_to_child.items():
            logging.debug("adding axis %s to child %s", axis, child.name)
            if child in children_axes:
                children_axes[child].add(axis)
            else:
                children_axes[child] = set([axis])

        # position & speed: special VAs combining multiple VAs
        self.position = model.VigilantAttribute(self._position, readonly=True)
        for c, ax in children_axes.items():

            def update_position_per_child(value, ax=ax, c=c):
                logging.debug("updating position of child %s", c.name)
                for a in ax:
                    try:
                        self._position[a] = value[axes_map[a]]
                    except KeyError:
                        logging.error(
                            "Child %s is not reporting position of axis %s",
                            c.name, a)
                self._updatePosition()

            logging.debug("Subscribing to position of child %s", c.name)
            c.position.subscribe(update_position_per_child)
            self._subfun.append(update_position_per_child)

        # TODO: change the speed range to a dict of speed ranges
        self.speed = model.MultiSpeedVA(self._speed, [0., 10.],
                                        setter=self._setSpeed)
        for c, ax in children_axes.items():
            if not (hasattr(child, "speed")
                    and isinstance(c.speed, model.VigilantAttributeBase)):
                continue

            def update_speed_per_child(value, ax=ax):
                for a in ax:
                    try:
                        self._speed[a] = value[axes_map[a]]
                    except KeyError:
                        logging.error(
                            "Child %s is not reporting speed of axis %s",
                            c.name, a)
                self._updateSpeed()

            c.speed.subscribe(update_speed_per_child)
            self._subfun.append(update_speed_per_child)

        # whether the axes are referenced
        self.referenced = model.VigilantAttribute(self._referenced,
                                                  readonly=True)
        for c, ax in children_axes.items():
            if not (hasattr(child, "referenced")
                    and isinstance(c.referenced, model.VigilantAttributeBase)):
                continue

            def update_ref_per_child(value, ax=ax):
                for a in ax:
                    try:
                        self._referenced[a] = value[axes_map[a]]
                    except KeyError:
                        logging.error(
                            "Child %s is not reporting reference of axis %s",
                            c.name, a)
                self._updateReferenced()

            c.referenced.subscribe(update_ref_per_child)
            self._subfun.append(update_ref_per_child)