def __init__(self, main): self.main = main # Streams available (handled by StreamController) # Note: we need to make sure ourselves that each stream in this # attribute is unique (i.e. only occurs once in the list). self.streams = model.ListVA() # Available Views. The are handled by the ViewController. # The `views` list basically keeps track of the relevant references. self.views = model.ListVA() # Current tool selected (from the toolbar, cf cont.tools) self.tool = None # Needs to be overridden by a IntEnumerated # The MicroscopeView currently focused, it is one of the `views` # or `None`. self.focussedView = VigilantAttribute(None) layouts = set( [VIEW_LAYOUT_ONE, VIEW_LAYOUT_22, VIEW_LAYOUT_FULLSCREEN]) self.viewLayout = model.IntEnumerated(VIEW_LAYOUT_22, choices=layouts) # The subset of views taken from `views` that *can* actually displayed, # but they might be hidden as well. # This attribute is also handled and manipulated by the ViewController. self.visible_views = model.ListVA()
def __init__(self, name, role, **kwargs): model.Emitter.__init__(self, name, role, **kwargs) self._shape = () self.power = model.FloatContinuous(0., [0., 100.], unit="W") self.power.subscribe(self._updatePower) # just one band: white # emissions is list of 0 <= floats <= 1. Always 1.0: cannot lower it. self.emissions = model.ListVA([1.0], unit="", setter=lambda x: [1.0]) self.spectra = model.ListVA( [(380e-9, 160e-9, 560e-9, 960e-9, 740e-9)], unit="m", readonly=True) # list of 5-tuples of floats
def __init__(self, name, role, ports, **kwargs): """ ports (string): pattern of the name of the serial ports to try to connect to find the devices. It can have a "glob", for example: "/dev/ttyUSB*" """ model.Emitter.__init__(self, name, role, **kwargs) self._ports = ports self._master, self._devices = self._getAvailableDevices(ports) if not self._devices: raise HwError( "No Omicron xX device found for ports '%s', check " "that '%s' is turned on and connected to the computer." % (ports, name)) spectra = [ ] # list of tuples: 99% low, 25% low, centre, 25% high, 99% high in m max_power = [] # list of float (W) for d in self._devices: spectra.append(d.wavelength) max_power.append(d.max_power) self._shape = () # power of the whole device (=> max power of the device with max power) self.power = model.FloatContinuous(0., (0., max(max_power)), unit="W") self.power.subscribe(self._updatePower) # ratio of power per device # => if some device don't support max power, clamped before 1 self.emissions = model.ListVA([0.] * len(self._devices), unit="", setter=self._setEmissions) # info on what device is which wavelength self.spectra = model.ListVA(spectra, unit="m", readonly=True) # Ensure the whole Hub is turned on if self._master: try: self._master.PowerOn() except OXXError: raise HwError( "Failed to power on the master device, check the interlock." ) # make sure everything is off (turning on the HUB will turn on the lights) self._updateIntensities(self.power.value, self.emissions.value) # set SW version driver_name = self._devices[0].acc.driver self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name)
def __init__(self, name, x, y, z, milling_angle, streams=None): """ :param name: (string) the feature name :param x: (float) the X axis of the feature position :param y: (float) the Y axis of the feature position :param z: (float) the Z axis of the feature position :param milling_angle: (float) angle used for milling (angle between the sample and the ion-beam, similar to the one in the chamber tab, not the actual Rx) :param streams: (List of StaticStream) list of acquired streams on this feature """ self.name = model.StringVA(name) # The 3D position of an interesting point in the site (Typically, the milling should happen around that # volume, never touching it.) self.pos = model.TupleContinuous( (x, y, z), range=((-1, -1, -1), (1, 1, 1)), cls=(int, float), ) # TODO: Check if negative milling angle is allowed if milling_angle <= 0: milling_angle = DEFAULT_MILLING_ANGLE logging.warning( f"Given milling angle {milling_angle} is negative, setting it to default {DEFAULT_MILLING_ANGLE}" ) self.milling_angle = model.FloatVA(milling_angle) self.status = model.StringVA(FEATURE_ACTIVE) # TODO: Handle acquired files self.streams = streams if streams is not None else model.ListVA()
def test_reproduceTypedValue_good(self): """ check various inputs and compare to expected output for values that should work """ lva = model.ListVA([12, -3]) # example value / input str / expected output tc = [(3, "-1561", -1561), (-9.3, "0.123", 0.123), (False, "true", True), ({"a": 12.5, "b": 3.}, "c:6,d:1.3", {"c": 6., "d":1.3}), ((-5, 0, 6), " 9, -8", (9, -8)), # we don't force to be the same size ((1.2, 0.0), "0, -8, -15e-3, 6.", (0.0, -8.0, -15e-3, 6.0)), ([1.2, 0.0], "0.1", [0.1]), (("cou", "bafd"), "aa,bb", ("aa", "bb")), # more complicated but nice to support for the user ((1200, 256), "256 x 256 px", (256, 256)), ((1.2, 256), " 21 x 0.2 m", (21, 0.2)), ([-5, 0, 6], "9,, -8", [9, -8]), ((1.2, 0.0), "", tuple()), (lva.value, "-1, 63, 12", [-1, 63, 12]), #NotifyingList becomes a list ((-5, 0, 6), "9.3, -8", (9, 3, -8)), # maybe this shouldn't work? # Note: we don't support SI prefixes (("cou",), "aa, c a", ("aa", " c a")), # TODO: need to see if spaces should be kept or trimmed ] for ex_val, str_val, expo in tc: out = reproduceTypedValue(ex_val, str_val) self.assertEqual(out, expo, "Testing with %s / '%s' -> %s" % (ex_val, str_val, out))
def __init__(self, main): assert main.microscope is not None MicroscopyGUIData.__init__(self, main) # Current tool selected (from the toolbar) tools = set([TOOL_NONE, TOOL_ZOOM, TOOL_ROI]) self.tool = IntEnumerated(TOOL_NONE, choices=tools) # Represent the global state of the microscopes. Mostly indicating # whether optical/sem streams are active. hw_states = {STATE_OFF, STATE_ON, STATE_DISABLED} if self.main.ccd: self.opticalState = model.IntEnumerated(STATE_OFF, choices=hw_states) if self.main.ebeam: self.emState = model.IntEnumerated(STATE_OFF, choices=hw_states) # history list of visited stage positions, ordered with latest visited # as last entry. self.stage_history = model.ListVA() # VA for autofocus procedure mode self.autofocus_active = BooleanVA(False)
def __init__(self, name, image): """ Note: parameters are different from the base class. image (DataArray of shape (111)YX): raw data. The metadata should contain at least MD_POS and MD_PIXEL_SIZE. It should also contain MD_IN_WL and MD_OUT_WL. """ # Wavelengths try: exc_range = image.metadata[model.MD_IN_WL] self.excitation = VigilantAttribute(exc_range, unit="m", readonly=True) except KeyError: logging.warning("No excitation wavelength for fluorescence stream") try: em_range = image.metadata[model.MD_OUT_WL] self.emission = VigilantAttribute(em_range, unit="m", readonly=True) default_tint = conversion.wave2rgb(numpy.mean(em_range)) except KeyError: logging.warning("No emission wavelength for fluorescence stream") default_tint = (0, 255, 0) # green is most typical # colouration of the image tint = image.metadata.get(model.MD_USER_TINT, default_tint) self.tint = model.ListVA(tint, unit="RGB") # 3-tuple R,G,B self.tint.subscribe(self.onTint) # Do it at the end, as it forces it the update of the image StaticStream.__init__(self, name, image)
def __init__(self, name, role, **kwargs): # TODO: allow the user to indicate the power and the spectrum via args? # This will create the .powerSupply VA model.Emitter.__init__(self, name, role, **kwargs) self.powerSupply.value = False # immediately turn it off self._shape = () self.power = model.FloatContinuous(0., (0., 10.), unit="W") self.power.subscribe(self._updatePower) # just one band: white # In general emissions is list of 0 <= floats <= 1. Here it's just one value, either 0 or 1. self.emissions = model.ListVA([0], unit="", setter=self._setEmissions) self.emissions.subscribe(self._updatePower) # TODO: update spectra VA to support the actual spectra of the lamp self.spectra = model.ListVA([(380e-9, 390e-9, 560e-9, 730e-9, 740e-9)], unit="m", readonly=True)
def __init__(self, name, role, sn=None, max_power=0.1, inversed=False, **kwargs): """ sn (None or str): serial number. If None, it will pick the first device found. max_power (0<float): maxium power emitted in W. """ model.Emitter.__init__(self, name, role, **kwargs) self._sn = sn self._max_power = max_power # Just find the first BlinkStick led controller if sn is None: self._bstick = blinkstick.find_first() else: # Note: doesn't work with v1.1.7: # need fix on get_string(), reported here: https://github.com/arvydas/blinkstick-python/pull/35 logging.warning( "Using sn to select the device doesn't currently work") self._bstick = blinkstick.find_by_serial(sn) if self._bstick is None: raise HwError( "Failed to find a Blinkstick for component %s. " "Check that the device is connected to the computer." % (name, )) self._bstick.set_inverse(inversed) time.sleep(0.1) # Device apparently needs some time to recover self._shape = () # list of 5-tuples of floats self.spectra = model.ListVA([(380e-9, 390e-9, 560e-9, 730e-9, 740e-9)], unit="m", readonly=True) self.power = model.ListContinuous([ 0., ], ((0., ), (max_power, )), unit="W", cls=(int, long, float), setter=self._setPower) self.power.subscribe(self._updatePower, init=True) self._swVersion = "Blinkstick v%s" % (blinkstick.__version__, ) # These functions report wrong values on Linux with v1.1.7 # man = self._bstick.get_manufacturer() # desc = self._bstick.get_description() # rsn = self._bstick.get_serial() man = self._bstick.device.manufacturer desc = self._bstick.device.product rsn = self._bstick.device.serial_number self._hwVersion = "%s %s (s/n: %s)" % (man, desc, rsn)
def __init__(self, name, role, dependencies, **kwargs): """ dependencies (dict str -> Emitter): arbitrary role -> emitter to be used as part of this emitter. All its provided emissions will be provided. """ # TODO: allow to only use a subset of the emissions from each child if not dependencies: raise ValueError("MultiplexLight needs dependencies") model.Emitter.__init__(self, name, role, dependencies=dependencies, **kwargs) self._shape = () self._child_idx = { } # Emitter -> index (shift) in the emissions/spectra spectra = [] for n, child in dependencies.items(): if not (model.hasVA(child, "power") and model.hasVA( child, "emissions") and model.hasVA(child, "spectra")): raise ValueError("Child %s is not a light emitter" % (n, )) self._child_idx[child] = len(spectra) spectra.extend(child.spectra.value) # TODO: update emissions whenever the child emissions change # Child with the maximum power range max_power = max(c.power.range[1] for c in self.dependencies.value) self.power = model.FloatContinuous(0, (0., max_power), unit="W") self.power.subscribe(self._updatePower) # info on which source is which wavelength self.spectra = model.ListVA(spectra, unit="m", readonly=True) # It needs .spectra and .power pwr, em = self._readPwrEmissions() self.power._value = pwr # ratio of power per source # if some source don't support max power, clamped before 1 self.emissions = model.ListVA(em, unit="", setter=self._setEmissions)
def __init__(self, name, role, ports, **kwargs): """ ports (string): pattern of the name of the serial ports to try to connect to find the devices. It can have a "glob", for example: "/dev/ttyUSB*" """ model.Emitter.__init__(self, name, role, **kwargs) self._ports = ports self._devices = self._getAvailableDevices(ports) if not self._devices: raise HwError("No Omicron xX device found for ports '%s', check " "they are turned on and connected to the computer." % ports) spectra = [] # list of tuples: 99% low, 25% low, centre, 25% high, 99% high in m max_power = [] # list of float (W) for d in self._devices: wl = d.wavelength # Lasers => spectrum is almost just one wl, but make it 2 nm wide # to avoid a bandwidth of exactly 0. spectra.append((wl - 1e-9, wl - 0.5e-9, wl, wl + 0.5e-9, wl + 1e-9)) max_power.append(d.max_power) self._shape = () # power of the whole device (=> max power of the device with max power) self.power = model.FloatContinuous(0., (0., max(max_power)), unit="W") self.power.subscribe(self._updatePower) # ratio of power per device # => if some device don't support max power, clamped before 1 self.emissions = model.ListVA([0.] * len(self._devices), unit="", setter=self._setEmissions) # info on what device is which wavelength self.spectra = model.ListVA(spectra, unit="m", readonly=True) # make sure everything is off self._updateIntensities(self.power.value, self.emissions.value) # set HW and SW version driver_name = driver.getSerialDriver(self._devices[0].port) self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name) self._hwVersion = "Omicron xX" # TODO: get version from GetFirmware()
def __init__(self, name, role, max_power=10.0, spectra=None, **kwargs): """ max_power (0 < float): the maximum power (in W) spectra (list of list of 5 tuple): output spectrum, as 5 wavelengths in m """ model.Emitter.__init__(self, name, role, **kwargs) self._shape = () self.power = model.FloatContinuous(0., (0., max_power), unit="W") self.power.subscribe(self._updatePower) # just one band: white # emissions is list of 0 <= floats <= 1. Always 1.0: cannot lower it. self.emissions = model.ListVA([1.0], unit="", setter=lambda x: [1.0]) # list of 5-tuples of floats if spectra is None: spectra = [(380e-9, 390e-9, 560e-9, 730e-9, 740e-9)] # White if len(spectra) != 1 or len(spectra[0]) != 5: raise ValueError("spectra argument must be a list of list of 5 values") self.spectra = model.ListVA([tuple(spectra[0])], unit="m", readonly=True)
def __init__(self, name, role, **kwargs): # TODO: allow the user to indicate the power and the spectrum via args? # This will create the .powerSupply VA model.Emitter.__init__(self, name, role, **kwargs) self.powerSupply.value = False # immediately turn it off self._shape = () self.power = model.ListContinuous([0], ((0,), (10,)), unit="W", cls=(int, long, float), setter=self._setPower) # just one band: white # TODO: update spectra VA to support the actual spectra of the lamp self.spectra = model.ListVA([(380e-9, 390e-9, 560e-9, 730e-9, 740e-9)], unit="m", readonly=True)
def duplicate_tab_data_model(self, orig): """ Duplicate a MicroscopyGUIData and adapt it for the acquisition window The streams will be shared, but not the views orig (MicroscopyGUIData) return (MicroscopyGUIData) """ # TODO: we'd better create a new view and copy the streams new = copy.copy(orig) # shallow copy new.streams = model.ListVA(orig.streams.value) # duplicate # create view (which cannot move or focus) view = guimodel.MicroscopeView("All") # differentiate it (only one view) new.views = model.ListVA() new.views.value.append(view) new.focussedView = model.VigilantAttribute(view) new.viewLayout = model.IntEnumerated(guimodel.VIEW_LAYOUT_ONE, choices={guimodel.VIEW_LAYOUT_ONE}) new.tool = model.IntEnumerated(TOOL_NONE, choices={TOOL_NONE}) return new
def create_cryo_tab_model(self): main = gmodel.MainGUIData(None) # no microscope backend # add role, features and currentFeature directly main.role = "cryo" main.features = omodel.ListVA() main.currentFeature = omodel.VigilantAttribute(None) tab = gmodel.CryoGUIData(main) # Add one view fview = gmodel.MicroscopeView("fakeview") tab.views.value.append(fview) tab.focussedView.value = fview return tab
def test_history_overlay(self): cnvs = miccanvas.DblMicroscopeCanvas(self.panel) cnvs.disable_drag() self.add_control(cnvs, wx.EXPAND, proportion=1, clear=True) history_va = omodel.ListVA() hol = vol.HistoryOverlay(cnvs, history_va) cnvs.add_view_overlay(hol) test.gui_loop() for j in range(4): offset = ((j - 2) * 75) for i in range(10): history_va.value.append( (((j * i) + offset, (j * -i) + offset), None) ) for i in range(10): history_va.value.append( (((j * -i) + offset, (j * -i) + offset), None) ) for i in range(10): history_va.value.append( (((j * -i) + offset, (j * i) + offset), None) ) for i in range(10): history_va.value.append( (((j * i) + offset, (j * i) + offset), None) ) test.gui_loop() steps = 1000 step_size = 10 for i in xrange(0, steps * step_size, step_size): phi = (math.pi * 2) / steps x = (100 * i / (steps * 5)) * math.cos(i * phi) y = (100 * i / (steps * 5)) * math.sin(i * phi) # hol.history.append(((x, y), None)) history_va.value.append( ((x, y), None) ) # print "Done generating history" test.gui_loop()
def test_pretty_str(self): prop = model.IntVA(2) pretty_str = str(prop) self.assertIn("IntVA", pretty_str) self.assertIn(str(prop.value), pretty_str) prop = model.ListVA([2, 3], unit=u"µm") pretty_str = str(prop) self.assertIn("ListVA", pretty_str) self.assertIn(str(prop.value), pretty_str) prop = model.FloatContinuous(2.3, unit=u"µm", range=(1.0, 9)) pretty_str = str(prop) self.assertIn("FloatContinuous", pretty_str) self.assertIn(str(prop.value), pretty_str)
def __init__(self, main): assert main.microscope is not None MicroscopyGUIData.__init__(self, main) # check there is something to move if not main.microscope.actuators: raise KeyError("No actuators found in the microscope") # Step size name -> val, range, actuator, axes (None if all) # str -> float, [float, float], str, (str, ...) ss_def = { "stage": (1e-6, [1e-8, 1e-3], "stage", None), "focus": (1e-7, [1e-8, 1e-4], "focus", None), "aligner": (1e-6, [1e-8, 1e-4], "aligner", None), # Mirror is a bit more complicated as it has 4 axes and Y # usually needs to be 10x bigger than X "mirror_x": (1e-6, [1e-8, 1e-3], "mirror", ("x", )), "mirror_y": (10e-6, [1e-8, 1e-3], "mirror", ("y", )), "mirror_r": (10e-6, [1e-8, 1e-3], "mirror", ("ry", "rz")) } # str -> VA: name (as the name of the attribute) -> step size (m) self.stepsizes = {} # This allow the UI code to mention axes only as role/axis name. # str -> (str, str): # role/axis ("mirror/x") -> (actuator ("mirror"), stepsize ("mirror_r")) self._axis_to_act_ss = {} # remove the ones that don't have an actuator for ss, (v, r, an, axn) in ss_def.items(): if getattr(main, an) is not None: self.stepsizes[ss] = model.FloatContinuous(v, r) if axn is None: axn = getattr(main, an).axes for a in axn: self._axis_to_act_ss[(an, a)] = (an, ss) logging.debug("Add axis %s/%s to stepsize %s", an, a, ss) # set of (str, str): actuator name, axis name self.axes = frozenset(self._axis_to_act_ss.keys()) # Tools are for lens alignment (mirror alignment actually needs none) tools = set([TOOL_NONE, TOOL_DICHO, TOOL_SPOT]) self.tool = IntEnumerated(TOOL_NONE, choices=tools) # For dichotomic mode self.dicho_seq = model.ListVA() # list of 4 enumerated for each corner
def __init__(self, name, role, max_power=10.0, spectra=None, **kwargs): """ max_power (0 < float): the maximum power (in W) spectra (list of list of 5 tuple): output spectrum, as 5 wavelengths in m """ model.Emitter.__init__(self, name, role, **kwargs) self._shape = () self.power = model.ListContinuous([0], ((0,), (max_power,)), unit="W", cls=(int, long, float), setter=self._setPower) # just one band: white # list of 5-tuples of floats if spectra is None: spectra = [(380e-9, 390e-9, 560e-9, 730e-9, 740e-9)] # White if len(spectra) != 1 or len(spectra[0]) != 5: raise ValueError("spectra argument must be a list of list of 5 values") self.spectra = model.ListVA([tuple(spectra[0])], unit="m", readonly=True)
def __init__(self, name, daemon): model.Component.__init__(self, name=name, daemon=daemon) self.executor = futures.ThreadPoolExecutor(max_workers=1) self.number_futures = 0 self.startAcquire = model.Event() # triggers when the acquisition of .data starts self.data = FakeDataFlow(sae=self.startAcquire) self.datas = SynchronizableDataFlow() self.data_count = 0 self._df = None # TODO automatically register the property when serializing the Component self.prop = model.IntVA(42) self.cont = model.FloatContinuous(2.0, [-1, 3.4], unit="C") self.enum = model.StringEnumerated("a", {"a", "c", "bfds"}) self.cut = model.IntVA(0, setter=self._setCut) self.listval = model.ListVA([2, 65])
def test_dichotomy_overlay(self): cnvs = miccanvas.SecomCanvas(self.panel) self.add_control(cnvs, wx.EXPAND, proportion=1, clear=True) lva = omodel.ListVA() dol = vol.DichotomyOverlay(cnvs, lva) cnvs.add_view_overlay(dol) def do_stuff(value): """ Test function that can be used to subscribe to VAs """ print "Testing VA subscriber received value ", value self.dummy = do_stuff dol.sequence_va.subscribe(do_stuff, init=True) dol.sequence_va.value = [0, 1, 2, 3, 0] dol.activate() test.gui_loop()
def __init__(self, name, role, dependencies, **kwargs): """ dependencies (dict str -> Emitter): arbitrary role -> emitter to be used as part of this emitter. All its provided emissions will be provided. """ # TODO: allow to only use a subset of the emissions from each child if not dependencies: raise ValueError("MultiplexLight needs dependencies") model.Emitter.__init__(self, name, role, dependencies=dependencies, **kwargs) self._shape = () self._child_idx = {} # Emitter -> index (shift) in the power/spectra spectra = [] min_power = [] max_power = [] for n, child in dependencies.items(): if not (model.hasVA(child, "power") and model.hasVA(child, "spectra") ): raise ValueError("Child %s is not a light emitter" % (n,)) self._child_idx[child] = len(spectra) spectra.extend(child.spectra.value) min_power.extend(child.power.range[0]) max_power.extend(child.power.range[1]) # Subscribe to each child power to update self.power child.power.subscribe(self._updateMultiplexPower) # Child with the maximum power range self.power = model.ListContinuous(value=[0] * len(spectra), range=(tuple(min_power), tuple(max_power)), unit="W", cls=(int, long, float)) self.power.subscribe(self._setChildPower) self._updateMultiplexPower(None) # info on which source is which wavelength self.spectra = model.ListVA(spectra, unit="m", readonly=True)
def __init__(self, name, detector, dataflow, emitter, focuser=None, opm=None, hwdetvas=None, hwemtvas=None, detvas=None, emtvas=None, raw=None, acq_type=None): """ name (string): user-friendly name of this stream detector (Detector): the detector which has the dataflow dataflow (Dataflow): the dataflow from which to get the data emitter (Emitter): the emitter opm (OpticalPathManager): the optical path manager focuser (Actuator or None): an actuator with a 'z' axis that allows to change the focus hwdetvas (None or set of str): names of all detector hardware VAs to be controlled by this Stream hwemtvas (None or set of str): names of all emitter hardware VAs to be controlled by this Stream detvas (None or set of str): names of all the detector VigilantAttributes (VAs) to be duplicated on the stream. They will be named .detOriginalName emtvas (None or set of str): names of all the emitter VAs to be duplicated on the stream. They will be named .emtOriginalName raw (None or list of DataArrays or DataArrayShadow): raw data to be used at initialisation. By default, it will contain no data. acq_type (MD_AT_*): acquisition type associated with this stream (as in model._metadata) """ self.name = model.StringVA(name) self.acquisitionType = model.VigilantAttribute(acq_type) # MD_ACQ_TYPE or None # for identification of the acquisition type associated with the stream # Hardware Components self._detector = detector self._emitter = emitter self._focuser = focuser self._opm = opm # Dataflow (Live image stream with meta data) # Note: A Detector can have multiple dataflows, so that's why a Stream # has a separate attribute. self._dataflow = dataflow # TODO: We need to reorganise everything so that the # image display is done via a dataflow (in a separate thread), instead # of a VA. self._im_needs_recompute = threading.Event() self._init_thread() # list of DataArray(Shadow) received and used to generate the image # every time it's modified, image is also modified if raw is None: self.raw = [] else: self.raw = raw # initialize the projected tiles cache self._projectedTilesCache = {} # initialize the raw tiles cache self._rawTilesCache = {} # TODO: should better be based on a BufferedDataFlow: subscribing starts # acquisition and sends (raw) data to whoever is interested. .get() # returns the previous or next image acquired. # indicating if stream has already been prepared self._prepared = False # TODO: should_update is a GUI stuff => move away from stream # should_update has no effect direct effect, it's just a flag to # indicate the user would like to have the stream updated (live) self.should_update = model.BooleanVA(False) # is_active set to True will keep the acquisition going on self.is_active = model.BooleanVA(False, setter=self._is_active_setter) # Leech to use during acquisition. # Note: for now only some streams actually use them (MDStreams*) self.leeches = [] # Hardware VA that the stream is directly linked to self.hw_vas = {} self.hw_vas.update(self._getVAs(detector, hwdetvas or set())) self.hw_vas.update(self._getVAs(emitter, hwemtvas or set())) # Duplicate VA if requested self._hwvas = {} # str (name of the proxied VA) -> original Hw VA self._hwvasetters = {} # str (name of the proxied VA) -> setter self._lvaupdaters = {} # str (name of the proxied VA) -> listener self._det_vas = self._duplicateVAs(detector, "det", detvas or set()) self._emt_vas = self._duplicateVAs(emitter, "emt", emtvas or set()) self._dRangeLock = threading.Lock() self._drange = None # min/max data range, or None if unknown self._drange_unreliable = True # if current values are a rough guess (based on detector) # drange_raw is the smaller (less zoomed) image of an pyramidal image. It is used # instead of the full image because it would be too slow or even impossible to read # the full data from the image to the memory. It is also not the tiles from the tiled # image, so the code for pyramidal and non-pyramidal images # that reads drange_raw is the same. # The drawback of not using the full image, is that some of the pixels are lost, so # maybe the max/min of the smaller image is different from the min/max of the full image. # And the histogram of both images will probably be a bit different also. if raw and isinstance(raw[0], model.DataArrayShadow): # if the image is pyramidal, use the smaller image drange_raw = self._getMergedRawImage(raw[0], raw[0].maxzoom) else: drange_raw = None # TODO: move to the DataProjection class self.auto_bc = model.BooleanVA(True) self.auto_bc.subscribe(self._onAutoBC) # % of values considered outliers discarded in auto BC detection # Note: 1/256th is a nice value because on RGB, it means in degenerated # cases (like flat histogram), you still loose only one value on each # side. self.auto_bc_outliers = model.FloatContinuous(100 / 256, range=(0, 40)) self.auto_bc_outliers.subscribe(self._onOutliers) self.tint = model.ListVA((255, 255, 255), unit="RGB") # 3-int R,G,B # Used if auto_bc is False # min/max ratio of the whole intensity level which are mapped to # black/white. Its range is ._drange (will be updated by _updateDRange) self.intensityRange = model.TupleContinuous((0, 0), range=((0, 0), (1, 1)), cls=(int, long, float), setter=self._setIntensityRange) # Make it so that the value gets clipped when its range is updated and # the value is outside of it. self.intensityRange.clip_on_range = True self._updateDRange(drange_raw) # sets intensityRange self._init_projection_vas() # Histogram of the current image _or_ slightly older image. # Note it's an ndarray. Use .tolist() to get a python list. self.histogram = model.VigilantAttribute(numpy.empty(0), readonly=True) self.histogram._full_hist = numpy.ndarray(0) # for finding the outliers self.histogram._edges = None # Tuple of (int, str) or (None, None): loglevel and message self.status = model.VigilantAttribute((None, None), readonly=True) # Background data, to be subtracted from the acquisition data before # projection. It should be the same shape and dtype as the acquisition # data, otherwise no subtraction will be performed. If None, nothing is # subtracted is applied. self.background = model.VigilantAttribute(None, setter=self._setBackground) self.background.subscribe(self._onBackground) # if there is already some data, update image with it # TODO: have this done by the child class, if needed. if self.raw: self._updateHistogram(drange_raw) self._onNewData(None, self.raw[0])
def test_list(self): prop = model.ListVA([2.0, 5, 4]) self.called = 0 # now count prop.subscribe(self.callback_test_notify, init=True) # +1 # List assignment prop.value = [3.0, 5] # +1 prop.value = list((0, )) # +1 prop.value = [0] # nothing because same value # Item removal del prop.value[0] # +1 self.assertEqual(prop.value, []) prop.value = [1, 2, 3, 4] # +1 del prop.value[1:-1] # +1 self.assertEqual(prop.value, [1, 4]) # Insert and remove item prop.value.insert(1, 66) # +1 self.assertEqual(prop.value, [1, 66, 4]) prop.value.remove(66) # +1 self.assertEqual(prop.value, [1, 4]) # Item adding prop.value += [44] # +1 self.assertEqual(prop.value, [1, 4, 44]) self.assertEqual(self.called, 9, "Called has value %s" % self.called) prop.value.extend([43, 42]) # +1 prop.value.extend( []) # The list value stays the same, so no increase!! self.assertEqual(prop.value, [1, 4, 44, 43, 42]) prop.value.append(41) # +1 self.assertEqual(prop.value, [1, 4, 44, 43, 42, 41]) # In place repetition orig_len = len(prop.value) prop.value *= 3 # +1 self.assertEqual(len(prop.value), orig_len * 3) self.assertEqual(self.called, 12, "Called has value %s" % self.called) # Item assignment prop.value = list(range(5)) # +1 prop.value[1] = 5 # +1 prop.value[1] = 5 # +0 self.assertEqual(prop.value, [0, 5, 2, 3, 4]) self.assertEqual(prop.value.pop(), 4) # +1 # Item sorting prop.value.sort() # +1 self.assertEqual(prop.value, [0, 2, 3, 5]) prop.value.reverse() # +1 self.assertEqual(prop.value, [5, 3, 2, 0]) # pl = pickle.dumps(prop.value, pickle.HIGHEST_PROTOCOL) # unpl = pickle.loads(pl) # self.assertEqual(unpl, prop.value) try: prop.value = 45 self.fail("Assigning int to a list should not be allowed.") except TypeError: pass # as it should be prop.unsubscribe(self.callback_test_notify) self.assertEqual(self.called, 17, "Called has value %s" % self.called) prop.value = ["b"] # no more counting self.assertEqual(prop.value, ["b"]) self.assertEqual(self.called, 17, "Called has value %s" % self.called)
def __init__(self, name, stage=None, focus0=None, focus1=None, stream_classes=None): """ :param name (string): user-friendly name of the view :param stage (Actuator): actuator with two axes: x and y :param focus0 (Actuator): actuator with one axis: z. Can be None :param focus1 (Actuator): actuator with one axis: z. Can be None Focuses 0 and 1 are modified when changing focus respectively along the X and Y axis. :param stream_classes (None, or tuple of classes): all subclasses that the streams in this view is allowed to show. """ super(MicroscopeView, self).__init__(name) if stream_classes is None: self.stream_classes = (Stream, ) else: self.stream_classes = stream_classes self._stage = stage self._focus = [focus0, focus1] # The real stage position, to be modified via moveStageToView() # it's a direct access from the stage, so looks like a dict of axes if stage: self.stage_pos = stage.position # stage.position.subscribe(self.onStagePos) # the current center of the view, which might be different from # the stage # TODO: we might need to have it on the MicroscopeModel, if all the # viewports must display the same location pos = self.stage_pos.value view_pos_init = (pos["x"], pos["y"]) else: view_pos_init = (0, 0) self.view_pos = model.ListVA(view_pos_init, unit="m") # current density (meter per pixel, ~ scale/zoom level) # 10µm/px => ~large view of the sample self.mpp = FloatContinuous(10e-6, range=(10e-12, 1e-3), unit="m/px") # How much one image is displayed on the other one. Value used by # StreamTree self.merge_ratio = FloatContinuous(0.3, range=[0, 1], unit="") self.merge_ratio.subscribe(self._onMergeRatio) # Streams to display (can be considered an implementation detail in most # cases) # Note: use addStream/removeStream for simple modifications self.stream_tree = StreamTree(merge=self.merge_ratio.value) # Only modify with this lock acquired: # TODO: Is this the source of the intermittent locking of the GUI when # Streams are active? If so, is there another/better way? self._streams_lock = threading.Lock() # TODO: list of annotations to display self.show_crosshair = model.BooleanVA(True)
def __init__(self, name, image, *args, **kwargs): """ name (string) image (model.DataArray(Shadow) of shape (CYX), (C11YX), (CTYX), (CT1YX), (1T1YX)). The metadata MD_WL_POLYNOMIAL or MD_WL_LIST should be included in order to associate the C to a wavelength. The metadata MD_TIME_LIST should be included to associate the T to a timestamp .background is a DataArray of shape (CT111), where C & T have the same length as in the data. .efficiencyCompensation is always DataArray of shape C1111. """ # Spectrum stream has in addition to normal stream: # * information about the current bandwidth displayed (avg. spectrum) # * coordinates of 1st point (1-point, line) # * coordinates of 2nd point (line) # TODO: need to handle DAS properly, in case it's tiled (in XY), to avoid # loading too much data in memory. # Ensure the data is a DataArray, as we don't handle (yet) DAS if isinstance(image, model.DataArrayShadow): image = image.getData() if len(image.shape) == 3: # force 5D for CYX image = image[:, numpy.newaxis, numpy.newaxis, :, :] elif len(image.shape) == 4: # force 5D for CTYX image = image[:, :, numpy.newaxis, :, :] elif len(image.shape) != 5 or image.shape[2] != 1: logging.error("Cannot handle data of shape %s", image.shape) raise NotImplementedError( "StaticSpectrumStream needs 3D or 4D data") # This is for "average spectrum" projection # cached list of wavelength for each pixel pos self._wl_px_values, unit_bw = spectrum.get_spectrum_range(image) min_bw, max_bw = self._wl_px_values[0], self._wl_px_values[-1] cwl = (max_bw + min_bw) / 2 width = (max_bw - min_bw) / 12 # The selected wavelength for a temporal spectrum display self.selected_wavelength = model.FloatContinuous( self._wl_px_values[0], range=(min_bw, max_bw), unit=unit_bw, setter=self._setWavelength) # Is there time data? if image.shape[1] > 1: # cached list of timestamps for each position in the time dimension self._tl_px_values, unit_t = spectrum.get_time_range(image) min_t, max_t = self._tl_px_values[0], self._tl_px_values[-1] # Allow the select the time as any value within the range, and the # setter will automatically "snap" it to the closest existing timestamp self.selected_time = model.FloatContinuous(self._tl_px_values[0], range=(min_t, max_t), unit=unit_t, setter=self._setTime) # This attribute is used to keep track of any selected pixel within the # data for the display of a spectrum self.selected_pixel = model.TupleVA((None, None)) # int, int # first point, second point in pixels. It must be 2 elements long. self.selected_line = model.ListVA([(None, None), (None, None)], setter=self._setLine) # The thickness of a point or a line (shared). # A point of width W leads to the average value between all the pixels # which are within W/2 from the center of the point. # A line of width W leads to a 1D spectrum taking into account all the # pixels which fit on an orthogonal line to the selected line at a # distance <= W/2. self.selectionWidth = model.IntContinuous(1, [1, 50], unit="px") self.selectionWidth.subscribe(self._onSelectionWidth) # Peak method index, None if spectrum peak fitting curve is not displayed self.peak_method = model.VAEnumerated("gaussian", {"gaussian", "lorentzian", None}) # TODO: allow to pass the calibration data as argument to avoid # recomputing the data just after init? # Spectrum efficiency compensation data: None or a DataArray (cf acq.calibration) self.efficiencyCompensation = model.VigilantAttribute( None, setter=self._setEffComp) self.efficiencyCompensation.subscribe(self._onCalib) # Is there spectrum data? if image.shape[0] > 1: # low/high values of the spectrum displayed self.spectrumBandwidth = model.TupleContinuous( (cwl - width, cwl + width), range=((min_bw, min_bw), (max_bw, max_bw)), unit=unit_bw, cls=(int, long, float)) self.spectrumBandwidth.subscribe(self.onSpectrumBandwidth) # Whether the (per bandwidth) display should be split intro 3 sub-bands # which are applied to RGB self.fitToRGB = model.BooleanVA(False) self.fitToRGB.subscribe(self.onFitToRGB) # the raw data after calibration self.calibrated = model.VigilantAttribute(image) if "acq_type" not in kwargs: if image.shape[0] > 1 and image.shape[1] > 1: kwargs["acq_type"] = model.MD_AT_TEMPSPECTRUM elif image.shape[0] > 1: kwargs["acq_type"] = model.MD_AT_SPECTRUM elif image.shape[1] > 1: kwargs["acq_type"] = model.MD_AT_TEMPORAL else: logging.warning( "SpectrumStream data has no spectrum or time dimension, shape = %s", image.shape) super(StaticSpectrumStream, self).__init__(name, [image], *args, **kwargs) # Automatically select point/line if data is small (can only be done # after .raw is set) if image.shape[-2:] == (1, 1): # Only one point => select it immediately self.selected_pixel.value = (0, 0) elif image.shape[ -2] == 1: # Horizontal line => select line immediately self.selected_line.value = [(0, 0), (image.shape[-1] - 1, 0)] elif image.shape[-1] == 1: # Vertical line => select line immediately self.selected_line.value = [(0, 0), (0, image.shape[-2] - 1)]
def __init__(self, name, role, port, sources, _serial=None, **kwargs): """ port (string): name of the serial port to connect to. Can be a pattern, in which case, all the ports fitting the pattern will be tried, and the first one which looks like an LLE will be used. sources (dict string -> 5-tuple of float): the light sources (by colour). The string is one of the seven names for the sources: "red", "cyan", "green", "UV", "yellow", "blue", "teal". They correspond to fix number in the LLE (cf documentation). The tuple contains the wavelength in m for the 99% low, 25% low, centre/max, 25% high, 99% high. They do no have to be extremely precise. The most important is the centre, and that they are all increasing values. If the device doesn't have the source it can be skipped. _serial (serial): for internal use only, directly use a opened serial connection """ # start with this opening the port: if it fails, we are done if _serial is not None: self._try_recover = False self._serial = _serial self._port = "" else: self._serial, self._port = self._findDevice(port) logging.info("Found LLE device on port %s", self._port) self._try_recover = True # to acquire before sending anything on the serial port self._ser_access = threading.Lock() # Init the LLE self._initDevice() if _serial is not None: # used for port testing => only simple init return # parse source and do some sanity check if not sources or not isinstance(sources, dict): logging.error( "sources argument must be a dict of source name -> wavelength 5 points" ) raise ValueError("Incorrect sources argument") self._source_id = [] # source number for each spectra self._gy = [] # indexes of green and yellow source self._rcubt = [] # indexes of other sources spectra = [] # list of the 5 wavelength points for cn, wls in sources.items(): cn = cn.lower() if cn not in COLOUR_TO_SOURCE: raise ValueError( "Sources argument contains unknown colour '%s'" % cn) if len(wls) != 5: raise ValueError( "Sources colour '%s' doesn't have exactly 5 wavelength points" % cn) prev_wl = 0 for wl in wls: if 0 > wl or wl > 100e-6: raise ValueError( "Sources colour '%s' has unexpected wavelength = %f nm" % (cn, wl * 1e9)) if prev_wl > wl: raise ValueError( "Sources colour '%s' has unsorted wavelengths" % cn) self._source_id.append(COLOUR_TO_SOURCE[cn]) if cn in ["green", "yellow"]: self._gy.append(len(spectra)) else: self._rcubt.append(len(spectra)) spectra.append(tuple(wls)) model.Emitter.__init__(self, name, role, **kwargs) self._shape = () self._max_power = 0.4 # W (According to doc: ~400mW) self.power = model.FloatContinuous(0., (0., self._max_power), unit="W") # emissions is list of 0 <= floats <= 1. self._intensities = [0.] * len(spectra) # start off self.emissions = model.ListVA(list(self._intensities), unit="", setter=self._setEmissions) self.spectra = model.ListVA(spectra, unit="m", readonly=True) self._prev_intensities = [None] * len( spectra) # => will update for sure self._updateIntensities() # turn off every source self.power.subscribe(self._updatePower) # set HW and SW version self._swVersion = "%s (serial driver: %s)" % ( odemis.__version__, driver.getSerialDriver(self._port)) self._hwVersion = "Lumencor Light Engine" # hardware doesn't report any version # Update temperature every 10s current_temp = self.GetTemperature() self.temperature = model.FloatVA(current_temp, unit=u"°C", readonly=True) self._temp_timer = util.RepeatingTimer(10, self._updateTemperature, "LLE temperature update") self._temp_timer.start()
def __init__(self, name, detector, dataflow, emitter): """ name (string): user-friendly name of this stream detector (Detector): the detector which has the dataflow dataflow (Dataflow): the dataflow from which to get the data emitter (Emitter): the emitter """ self.name = model.StringVA(name) # Hardware Components self._detector = detector self._emitter = emitter # Dataflow (Live image stream with meta data) # Note: A Detectors can have multiple dataflows, so that's why a Stream # has a separate attribute. self._dataflow = dataflow # TODO: this flag is horrendous as it can lead to not updating the image # with the latest image. We need to reorganise everything so that the # image display is done via a dataflow (in a separate thread), instead # of a VA. self._running_upd_img = False # to avoid simultaneous updates in different threads # list of DataArray received and used to generate the image # every time it's modified, image is also modified self.raw = [] # the most important attribute self.image = model.VigilantAttribute(None) # TODO: should maybe to 2 methods activate/deactivate to explicitly # start/stop acquisition, and one VA "updated" to stated that the user # want this stream updated (as often as possible while other streams are # also updated) # should_update has no effect direct effect, it's just a flag to # indicate the user would like to have the stream updated (live) self.should_update = model.BooleanVA(False) # is_active set to True will keep the acquisition going on self.is_active = model.BooleanVA(False) self.is_active.subscribe(self.onActive) # Region of interest as left, top, right, bottom (in ratio from the # whole area of the emitter => between 0 and 1) self.roi = model.TupleContinuous((0, 0, 1, 1), range=((0, 0, 0, 0), (1, 1, 1, 1)), cls=(int, long, float)) self._drange = None # min/max data range, or None if unknown # whether to use auto brightness & contrast self.auto_bc = model.BooleanVA(True) # % of values considered outliers discarded in auto BC detection # Note: 1/256th is a nice value because on RGB, it means in degenerated # cases (like flat histogram), you still loose only one value on each # side. self.auto_bc_outliers = model.FloatContinuous(100 / 256, range=(0, 40)) # Used if auto_bc is False # min/max ratio of the whole intensity level which are mapped to # black/white. Its range is ._drange (will be updated by _updateDRange) self.intensityRange = model.TupleContinuous( (0, 0), range=((0, 0), (1, 1)), cls=(int, long, float), setter=self._setIntensityRange) # Make it so that the value gets clipped when its range is updated and # the value is outside of it. self.intensityRange.clip_on_range = True self._updateDRange() # Histogram of the current image _or_ slightly older image. # Note it's an ndarray. Use .tolist() to get a python list. self.histogram = model.VigilantAttribute(numpy.empty(0), readonly=True) self.histogram._full_hist = numpy.ndarray( 0) # for finding the outliers self.histogram._edges = None self.auto_bc.subscribe(self._onAutoBC) self.auto_bc_outliers.subscribe(self._onOutliers) self.intensityRange.subscribe(self._onIntensityRange) self._ht_needs_recompute = threading.Event() self._hthread = threading.Thread(target=self._histogram_thread, name="Histogram computation") self._hthread.daemon = True self._hthread.start() # self.histogram.subscribe(self._onHistogram) # FIXME -> update outliers and then image # list of warnings to display to the user # TODO should be a set self.warnings = model.ListVA([]) # should only contain WARNING_*
def __init__(self, name, image): """ name (string) image (model.DataArray of shape (CYX) or (C11YX)). The metadata MD_WL_POLYNOMIAL should be included in order to associate the C to a wavelength. """ self._calibrated = None # just for the _updateDRange to not complain Stream.__init__(self, name, None, None, None) # Spectrum stream has in addition to normal stream: # * information about the current bandwidth displayed (avg. spectrum) # * coordinates of 1st point (1-point, line) # * coordinates of 2nd point (line) if len(image.shape) == 3: # force 5D image = image[:, numpy.newaxis, numpy.newaxis, :, :] elif len(image.shape) != 5 or image.shape[1:3] != (1, 1): logging.error("Cannot handle data of shape %s", image.shape) raise NotImplementedError("SpectrumStream needs a cube data") # ## this is for "average spectrum" projection try: # cached list of wavelength for each pixel pos self._wl_px_values = spectrum.get_wavelength_per_pixel(image) except (ValueError, KeyError): # useless polynomial => just show pixels values (ex: -50 -> +50 px) # TODO: try to make them always int? max_bw = image.shape[0] // 2 min_bw = (max_bw - image.shape[0]) + 1 self._wl_px_values = range(min_bw, max_bw + 1) assert (len(self._wl_px_values) == image.shape[0]) unit_bw = "px" cwl = (max_bw + min_bw) // 2 width = image.shape[0] // 12 else: min_bw, max_bw = self._wl_px_values[0], self._wl_px_values[-1] unit_bw = "m" cwl = (max_bw + min_bw) / 2 width = (max_bw - min_bw) / 12 # TODO: allow to pass the calibration data as argument to avoid # recomputing the data just after init? # Spectrum efficiency compensation data: None or a DataArray (cf acq.calibration) self.efficiencyCompensation = model.VigilantAttribute( None, setter=self._setEffComp) # The background data (typically, an acquisition without ebeam). # It is subtracted from the acquisition data. # If set to None, a simple baseline background value is subtracted. self.background = model.VigilantAttribute(None, setter=self._setBackground) # low/high values of the spectrum displayed self.spectrumBandwidth = model.TupleContinuous( (cwl - width, cwl + width), range=((min_bw, min_bw), (max_bw, max_bw)), unit=unit_bw, cls=(int, long, float)) # Whether the (per bandwidth) display should be split intro 3 sub-bands # which are applied to RGB self.fitToRGB = model.BooleanVA(False) self._drange = None # This attribute is used to keep track of any selected pixel within the # data for the display of a spectrum self.selected_pixel = model.TupleVA((None, None)) # int, int # first point, second point in pixels. It must be 2 elements long. self.selected_line = model.ListVA([(None, None), (None, None)], setter=self._setLine) # The thickness of a point of a line (shared). # A point of width W leads to the average value between all the pixels # which are within W/2 from the center of the point. # A line of width W leads to a 1D spectrum taking into account all the # pixels which fit on an orthogonal line to the selected line at a # distance <= W/2. self.width = model.IntContinuous(1, [1, 50], unit="px") self.fitToRGB.subscribe(self.onFitToRGB) self.spectrumBandwidth.subscribe(self.onSpectrumBandwidth) self.efficiencyCompensation.subscribe(self._onCalib) self.background.subscribe(self._onCalib) self.raw = [image ] # for compatibility with other streams (like saving...) self._calibrated = image # the raw data after calibration self._updateDRange() self._updateHistogram() self._updateImage()
def __init__(self, name, role, device, channels, spectra, pwr_curve, **kwargs): """ device (string): name of the /dev comedi device (ex: "/dev/comedi0") channels (list of (0<=int)): The output channel for each source, as numbered in the comedi subdevice. spectra (list of 5-tuple of float): the spectra for each output channel used. Each tuple represents the wavelength in m for the 99% low, 25% low, centre/max, 25% high, 99% high. They do no have to be extremely precise. The most important is the centre, and that they are all increasing values. pwr_curve (list of dict (float -> 0<float)): Power curve segment map for each source. A segment map is a series of voltage output on the analog output -> emission power of the light (W). It represents a series of linear segments to map the voltage output to the light emission. At least one pair should be provided. If no voltage is linked to 0W, then a 0V -> 0W mapping is used. The total curve should be monotonic. """ # TODO: allow to give the unit of the power/pwr_curve ? model.Emitter.__init__(self, name, role, **kwargs) self._shape = () try: self._device = comedi.open(device) # self._fileno = comedi.fileno(self._device) except comedi.ComediError: raise ValueError("Failed to open DAQ device '%s'" % device) # Look for the analog output subdevice try: self._ao_subd = comedi.find_subdevice_by_type( self._device, comedi.SUBD_AO, 0) nchan = comedi.get_n_channels(self._device, self._ao_subd) if nchan < max(channels): raise ValueError( "Device only has %d channels, while needed %d" % (nchan, max(channels))) except comedi.ComediError: raise ValueError( "Failed to find an analogue output on DAQ device '%s'" % device) if len(channels) != len(spectra): raise ValueError( "spectra argument should have the same length as channels (%d)" % len(channels)) if len(channels) != len(pwr_curve): raise ValueError( "pwr_curve argument should have the same length as channels (%d)" % len(channels)) self._channels = channels # Check and store the power curves self._ranges = [] self._pwr_curve = [] for c, crv in zip(channels, pwr_curve): crv = [v for v in crv.items()] # Add 0W = 0V if nothing = 0W if 0 not in [w for v, w in crv]: crv.append((0, 0)) logging.info( "Adding 0V -> 0W mapping to pwr_curve for channel %d", c) # At least beginning and end values if len(crv) < 2: raise ValueError( "pwr_curve for channel %d has less than 2 values: %s" % (c, crv)) # Check it's monotonic crv = sorted(crv, key=lambda v: v[0]) if crv[0][1] < 0: raise ValueError( "pwr_curve for channel %d has negative power: %g W" % (c, crv[0][1])) if len(crv) != len(set(v for v, w in crv)): raise ValueError( "pwr_curve for channel %d has identical voltages: %s" % (c, crv)) if not all( (crv[i][1] < crv[i + 1][1]) for i in range(len(crv) - 1)): raise ValueError( "pwr_curve for channel %d is not monotonic: %s" % (c, crv)) self._pwr_curve.append(crv) # Find the best range to use try: ri = comedi.find_range(self._device, self._ao_subd, c, comedi.UNIT_volt, crv[0][0], crv[-1][0]) except comedi.ComediError: raise ValueError( "Data range between %g and %g V is too high for hardware." % (crv[0][0], crv[-1][0])) self._ranges.append(ri) # Check the spectra spect = [] # list of the 5 wavelength points for c, wls in zip(channels, spectra): if len(wls) != 5: raise ValueError( "Spectra for channel %d doesn't have exactly 5 wavelength points: %s" % (c, wls)) if list(wls) != sorted(wls): raise ValueError( "Spectra for channel %d has unsorted wavelengths: %s" % (c, wls)) for wl in wls: if not 0 < wl < 100e-6: raise ValueError( "Spectra for channel %d has unexpected wavelength = %f nm" % (c, wl * 1e9)) spect.append(tuple(wls)) # Maximum power for channel to be used as a range for power max_power = tuple([crv[-1][1] for crv in self._pwr_curve]) # Power value for each channel of the device self.power = model.ListContinuous( value=[0.] * len(self._channels), range=( tuple([0.] * len(self._channels)), max_power, ), unit="W", cls=(int, long, float), ) self.power.subscribe(self._updatePower) # info on which channel is which wavelength self.spectra = model.ListVA(spect, unit="m", readonly=True) # make sure everything is off (turning on the HUB will turn on the lights) self.power.value = self.power.range[0] self._metadata = {model.MD_HW_NAME: self.getHwName()} lnx_ver = driver.get_linux_version() self._swVersion = "%s (driver %s, linux %s)" % ( odemis.__version__, self.getSwVersion(), ".".join( "%s" % v for v in lnx_ver)) self._metadata[model.MD_SW_VERSION] = self._swVersion self._metadata[model.MD_HW_VERSION] = self._hwVersion # unknown