def test_resolution(self): va = model.ResolutionVA((10, 10), ((1, 1), (100, 150))) self.assertEqual(va.value, (10, 10)) self.assertEqual(va.range, ((1, 1), (100, 150))) # must convert anything to a tuple va.value = [11, 150] self.assertEqual(va.value, (11, 150)) # Numpy integers are also fine va.value = (numpy.prod([10, 3]), numpy.uint64(1)) # must not accept resolutions with float try: va.value = (8., 160) self.fail("Assigning non int values should not be allowed.") except TypeError: pass # as it should be # must not accept resolutions outside of the range try: va.value = (80, 160) self.fail("Assigning value not in range should not be allowed.") except IndexError: pass # as it should be try: va.value = (10, 10, 10) self.fail( "Assigning a 3-tuple to a resolution should not be allowed.") except TypeError: pass # as it should be
def __init__(self, fake_img): """ Use .fake_img to change the image sent by the ccd Args: fake_img: 2D DataArray """ super(FakeCCD, self).__init__("testccd", "ccd") self.exposureTime = model.FloatContinuous(0.1, (1e-6, 1000), unit="s") res = fake_img.shape[1], fake_img.shape[0] # X, Y depth = 2 ** (fake_img.dtype.itemsize * 8) self.shape = (res[0], res[1], depth) self.binning = model.TupleContinuous((1, 1), [(1, 1), (8, 8)], cls=(int, long, float), unit="") self.resolution = model.ResolutionVA(res, [(1, 1), res]) self.readoutRate = model.FloatVA(1e9, unit="Hz", readonly=True) pxs_sens = fake_img.metadata.get(model.MD_SENSOR_PIXEL_SIZE, (10e-6, 10e-6)) self.pixelSize = model.VigilantAttribute(pxs_sens, unit="m", readonly=True) self.data = CCDDataFlow(self) self._acquisition_thread = None self._acquisition_lock = threading.Lock() self._acquisition_init_lock = threading.Lock() self._acquisition_must_stop = threading.Event() self.fake_img = fake_img self._metadata = fake_img.metadata
def __init__(self, name, role, mag, pole_pos=None, **kwargs): """ name (string): should be the name of the product (for metadata) mag (float > 0): magnification ratio pole_pos (2 floats > 0): position of the pole on the CCD (in px, without binning). Used for angular resolved imaging on SPARC (only). cf MD_AR_POLE """ assert (mag > 0) model.HwComponent.__init__(self, name, role, **kwargs) self._swVersion = "N/A (Odemis %s)" % odemis.__version__ self._hwVersion = name # allow the user to modify the value, if the lens is manually changed self.magnification = model.FloatContinuous(mag, range=[1e-3, 1e6], unit="") if pole_pos is not None: if (not isinstance(pole_pos, collections.Iterable) or len(pole_pos) != 2 or any(v < 0 for v in pole_pos)): raise ValueError("pole_pos must be 2 positive values, got %s" % pole_pos) self.polePosition = model.ResolutionVA(pole_pos, range=[(0, 0), (1e6, 1e6)])
def __init__(self, name, role, parent, **kwargs): """ Initialises the device. Raise an exception if the device cannot be opened. """ model.DigitalCamera.__init__(self, name, role, parent=parent, **kwargs) # Parameters explanation: Chamber camera enabled on channel 0 (reserved), # without zoom applied so we get the maximum size of the image (1), # in the maximum fps (5), and compression mode 0 (must be so, according, # to Tescan API documentation). self.parent._device.CameraEnable(0, 1, 5, 0) # Wait for camera to be enabled while (self.parent._device.CameraGetStatus(0))[0] != 1: time.sleep(0.5) # Get a first image to determine the resolution width, height, img_str = self.parent._device.FetchCameraImage(0) self.parent._device.CameraDisable() resolution = (height, width) self._shape = resolution + (2**8, ) self.resolution = model.ResolutionVA(resolution, [(1, 1), (2048, 2048)], readonly=True) self.acquisition_lock = threading.Lock() self.acquire_must_stop = threading.Event() self.acquire_thread = None self.data = ChamberDataFlow(self) logging.debug("Camera component ready to use.")
def __init__(self, name, ccd, emitter, emd): """ name (string): user-friendly name of this stream ccd (Camera): the ccd emitter (Emitter): the emitter (eg: ebeam scanner) emd (Detector): the SEM detector (eg: SED) """ self.name = model.StringVA(name) # Hardware Components self._detector = emd self._emitter = emitter self._ccd = ccd # 0.1s is a bit small, but the algorithm will automaticaly try with # longer dwell times if no spot is visible first. self.dwellTime = model.FloatContinuous(0.1, range=emitter.dwellTime.range, unit="s") # The number of points in the grid self.repetition = model.ResolutionVA( (4, 4), # good default range=((2, 2), (16, 16))) # Future generated by find_overlay self._overlay_future = None
def __init__(self, name): model.Emitter.__init__(self, name, "fakeebeam", parent=None) self._shape = (2048, 2048) self.resolution = model.ResolutionVA((256, 256), [(1, 1), self._shape]) self.pixelSize = model.VigilantAttribute((1e-9, 1e-9), unit="m", readonly=True) self.magnification = model.FloatVA(1000.)
def __init__(self, name, detector, dataflow, emitter): self.name = model.StringVA(name) # Hardware Components self._detector = detector # the spectrometer self._emitter = emitter # the e-beam # To acquire simultaneously other detector (ex: SEM secondary electrons) # a separate stream must be used, and the acquisition manager will take # care of doing both at the same time # data-flow of the spectrometer self._dataflow = dataflow self.raw = [] # to contain data during acquisition (from MD streams) # all the information needed to acquire an image (in addition to the # hardware component settings which can be directly set). # ROI + repetition is sufficient, but pixel size is nicer for the user # and allow us to ensure each pixel is square. (Non-square pixels are # not a problem for the hardware, but annoying to display data in normal # software). # We ensure in the setters that all the data is always consistent: # roi set: roi + pxs → repetition + roi + pxs # pxs set: roi + pxs → repetition + roi (small changes) # repetition set: repetition + roi + pxs → repetition + pxs + roi (small changes) # 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), setter=self._setROI) # the number of pixels acquired in each dimension # it will be assigned to the resolution of the emitter (but cannot be # directly set, as one might want to use the emitter while configuring # the stream). self.repetition = model.ResolutionVA(emitter.resolution.value, emitter.resolution.range, setter=self._setRepetition) # the size of the pixel, horizontally and vertically # actual range is dynamic, as it changes with the magnification self.pixelSize = model.FloatContinuous(emitter.pixelSize.value[0], range=[0, 1], unit="m", setter=self._setPixelSize) # exposure time of each pixel is the exposure time of the detector, # the dwell time of the emitter will be adapted before acquisition. # Update the pixel size whenever SEM magnification changes # This allows to keep the ROI at the same place in the SEM FoV. # Note: this is to be done only if the user needs to manually update the # magnification. self._prev_mag = emitter.magnification.value emitter.magnification.subscribe(self._onMagnification)
def __init__(self, name, role, parent, **kwargs): ''' parent (symphotime.Controller): a symphotime server parent object ''' # we will fill the set of children with Components later in ._children model.Emitter.__init__(self, name, role, parent=parent, **kwargs) self._shape = (2048, 2048) # Max resolution # Define VA's as references to the parent. self.filename = model.StringVA(setter=self._setFilename) self.directory = model.StringVA(setter=self._setDirectory) self.resolution = model.ResolutionVA((64, 64), ((1, 1), (2048, 2048))) self.bidirectional = model.BooleanVA(value=False) self.dwellTime = model.FloatContinuous(value=10e-6, range=DWELLTIME_RNG, unit="s")
def __init__(self, name): model.Emitter.__init__(self, name, "fakeebeam", parent=None) self._shape = (2048, 2048) self.dwellTime = model.FloatContinuous(1e-6, (1e-6, 1), unit="s") self.resolution = model.ResolutionVA((256, 256), [(1, 1), self._shape]) self.pixelSize = model.VigilantAttribute((1e-9, 1e-9), unit="m", readonly=True) self.magnification = model.FloatVA(1000.) self.scale = model.TupleContinuous((1, 1), [(1, 1), self._shape], cls=(int, long, float), unit="") self.translation = model.TupleContinuous((0, 0), ((0, 0), (0, 0)), cls=(int, long, float), unit="px")
def __init__(self, testCase, align): super(FakeCCD, self).__init__("testccd", "ccd") self.testCase = testCase self.align = align self.exposureTime = model.FloatContinuous(1, (1e-6, 1000), unit="s") self.binning = model.TupleContinuous((1, 1), [(1, 1), (8, 8)], cls=(int, long, float), unit="") self.resolution = model.ResolutionVA((2160, 2560), [(1, 1), (2160, 2560)]) self.data = CCDDataFlow(self) self._acquisition_thread = None self._acquisition_lock = threading.Lock() self._acquisition_init_lock = threading.Lock() self._acquisition_must_stop = threading.Event() self.fake_img = self.testCase.fake_img
def __init__(self, fake_img): """ Use .fake_img to change the image sent by the ccd Args: fake_img: 2D DataArray """ super(FakeCCD, self).__init__("testccd", "ccd") self.exposureTime = model.FloatContinuous(0.1, (1e-6, 1000), unit="s") res = fake_img.shape[1], fake_img.shape[0] # X, Y self.binning = model.TupleContinuous((1, 1), [(1, 1), (8, 8)], cls=(int, long, float), unit="") self.resolution = model.ResolutionVA(res, [(1, 1), res]) self.data = CCDDataFlow(self) self._acquisition_thread = None self._acquisition_lock = threading.Lock() self._acquisition_init_lock = threading.Lock() self._acquisition_must_stop = threading.Event() self.fake_img = fake_img
def __init__(self, name, role, parent, **kwargs): # It will set up ._shape and .parent model.Emitter.__init__(self, name, role, parent=parent, **kwargs) fake_img = self.parent.fake_img if parent._drift_period: # half the size, to keep some margin for the drift self._shape = tuple(v // 2 for v in fake_img.shape[::-1]) else: self._shape = fake_img.shape[::-1] # next two values are just to determine the pixel size # Distance between borders if magnification = 1. It should be found out # via calibration. We assume that image is square, i.e., VFV = HFV self._hfw_nomag = 0.25 # m # pixelSize is the same as MD_PIXEL_SIZE, with scale == 1 # == smallest size/ between two different ebeam positions pxs = fake_img.metadata[model.MD_PIXEL_SIZE] self.pixelSize = model.VigilantAttribute(pxs, unit="m", readonly=True) # the horizontalFoV VA indicates that it's possible to control the zoom hfv = pxs[0] * self._shape[0] self.horizontalFoV = model.FloatContinuous(hfv, range=[1e-6, 10e-3], unit="m") self.magnification = model.VigilantAttribute(self._hfw_nomag / hfv, unit="", readonly=True) # (.resolution), .translation, .rotation, and .scaling are used to # define the conversion from coordinates to a region of interest. # (float, float) in px => moves center of acquisition by this amount # independent of scale and rotation. tran_rng = [(-self._shape[0] / 2, -self._shape[1] / 2), (self._shape[0] / 2, self._shape[1] / 2)] self.translation = model.TupleContinuous((0, 0), tran_rng, cls=(int, long, float), unit="px", setter=self._setTranslation) # .resolution is the number of pixels actually scanned. If it's less than # the whole possible area, it's centered. resolution = (self._shape[0] // 8, self._shape[1] // 8) self.resolution = model.ResolutionVA(resolution, [(1, 1), self._shape], setter=self._setResolution) self._resolution = resolution # (float, float) as a ratio => how big is a pixel, compared to pixelSize # it basically works the same as binning, but can be float # (Default to scan the whole area) self._scale = (self._shape[0] / resolution[0], self._shape[1] / resolution[1]) self.scale = model.TupleContinuous(self._scale, [(1, 1), self._shape], cls=(int, long, float), unit="", setter=self._setScale) self.scale.subscribe(self._onScale, init=True) # to update metadata # (float) in rad => rotation of the image compared to the original axes # TODO: for now it's readonly because no rotation is supported self.rotation = model.FloatContinuous(0, [0, 2 * math.pi], unit="rad", readonly=True) self.dwellTime = model.FloatContinuous(1e-06, (1e-06, 1000), unit="s") # VAs to control the ebeam, purely fake self.power = model.FloatEnumerated(1, {0, 1}) self.probeCurrent = model.FloatEnumerated(1.3e-9, {0.1e-9, 1.3e-9, 2.6e-9, 3.4e-9, 11.564e-9, 23e-9}, unit="A") self.accelVoltage = model.FloatContinuous(10e6, (1e6, 30e6), unit="V")
def __init__(self, name, role, children=None, daemon=None, **kwargs): """ All the arguments are identical to AndorCam2, expected: children (dict string->kwargs): name of child must be "shamrock" and the kwargs contains the arguments passed to instantiate the Shamrock component """ # we will fill the set of children with Components later in ._children model.Detector.__init__(self, name, role, daemon=daemon, **kwargs) # Create the detector (ccd) child try: dt_kwargs = children["andorcam2"] except Exception: raise ValueError("AndorSpec excepts one child named 'andorcam2'") # We could inherit from it, but difficult to not mix up .binning, .shape # .resolution... self._detector = andorcam2.AndorCam2(daemon=daemon, **dt_kwargs) self._children.add(self._detector) dt = self._detector # Copy and adapt the VAs and roattributes from the detector # set up the detector part # check that the shape is "horizontal" if dt.shape[0] <= 1: raise ValueError( "Child detector must have at least 2 pixels horizontally") if dt.shape[0] < dt.shape[1]: logging.warning( "Child detector is shaped vertically (%dx%d), " "this is probably incorrect, as wavelengths are " "expected to be along the horizontal axis", dt.shape[0], dt.shape[1]) # shape is same as detector (raw sensor), but the max resolution is always flat self._shape = tuple(dt.shape) # duplicate # The resolution and binning are derived from the detector, but with # settings set so that there is only one horizontal line. if dt.binning.range[1][1] < dt.resolution.range[1][1]: # without software binning, we are stuck to the max binning logging.info( "Spectrometer %s will only use a %d px band of the %d " "px of the sensor", name, dt.binning.range[1][1], dt.resolution.range[1][1]) resolution = (dt.resolution.range[1][0], 1) # max,1 # vertically: 1, with binning as big as possible binning = (dt.binning.value[0], min(dt.binning.range[1][1], dt.resolution.range[1][1])) min_res = (dt.resolution.range[0][0], 1) max_res = (dt.resolution.range[1][0], 1) self.resolution = model.ResolutionVA(resolution, [min_res, max_res], setter=self._setResolution) # 2D binning is like a "small resolution" self._binning = binning self.binning = model.ResolutionVA(self._binning, dt.binning.range, setter=self._setBinning) self._setBinning(binning) # will also update the resolution # TODO: update also the metadata MD_SENSOR_PIXEL_SIZE pxs = dt.pixelSize.value[ 0], dt.pixelSize.value[1] * dt.binning.value[1] self.pixelSize = model.VigilantAttribute(pxs, unit="m", readonly=True) # Note: the metadata has no MD_PIXEL_SIZE, but a MD_WL_LIST # TODO: support software binning by rolling up our own dataflow that # does data merging assert dt.resolution.range[0][1] == 1 self.data = dt.data # duplicate every other VA and Event from the detector # that includes required VAs like .exposureTime for aname, value in model.getVAs(dt).items() + model.getEvents( dt).items(): if not hasattr(self, aname): setattr(self, aname, value) else: logging.debug( "skipping duplication of already existing VA '%s'", aname) assert hasattr(self, "exposureTime") # Create the spectrograph (actuator) child try: sp_kwargs = children["shamrock"] except Exception: raise ValueError("AndorSpec excepts one child named 'shamrock'") self._spectrograph = Shamrock(parent=self, path=self._detector._initpath, daemon=daemon, **sp_kwargs) self._children.add(self._spectrograph) self._spectrograph.position.subscribe(self._onPositionUpdate) self.resolution.subscribe(self._onResBinningUpdate) self.binning.subscribe(self._onResBinningUpdate, init=True)
def __init__(self, name, role, device=None, children=None, daemon=None, disc_volt=None, zero_cross=None, **kwargs): """ device (None or str): serial number (eg, 1020345) of the device to use or None if any device is fine. children (dict str -> kwargs): the names of the detectors (detector0 and detector1 are valid) to the arguments. disc_volt (2 (0 <= float <= 0.8)): discriminator voltage for the APD 0 and 1 (in V) zero_cross (2 (0 <= float <= 2e-3)): zero cross voltage for the APD0 and 1 (in V) """ if children is None: children = {} if device == "fake": device = None self._dll = FakePHDLL() else: self._dll = PHDLL() self._idx = self._openDevice(device) if disc_volt is None: disc_volt = [0, 0] if zero_cross is None: zero_cross = [0, 0] super(PH300, self).__init__(name, role, daemon=daemon, **kwargs) # TODO: metadata for indicating the range? cf WL_LIST? # TODO: do we need TTTR mode? self.Initialise(MODE_HIST) self._swVersion = self.GetLibraryVersion() self._metadata[model.MD_SW_VERSION] = self._swVersion mod, partnum, ver = self.GetHardwareInfo() sn = self.GetSerialNumber() self._hwVersion = "%s %s %s (s/n %s)" % (mod, partnum, ver, sn) self._metadata[model.MD_HW_VERSION] = self._hwVersion self._metadata[model.MD_DET_TYPE] = model.MD_DT_NORMAL logging.info("Opened device %d (%s s/n %s)", self._idx, mod, sn) self.Calibrate() # Do basic set-up for things that should never be needed to change self.SetSyncDiv(1) # 1 = no divider TODO: needs to be a VA? # TODO: needs to be changeable? self.SetOffset(0) # To pass the raw count of each detector, we create children detectors. # It could also go into just separate DataFlow, but then it's difficult # to allow using these DataFlows in a standard way. self._detectors = {} for name, ckwargs in children.items(): if name == "detector0": i = 0 elif name == "detector1": i = 1 else: raise ValueError("") self._detectors[name] = PH300RawDetector(channel=i, parent=self, daemon=daemon, **ckwargs) self.children.value.add(self._detectors[name]) # dwellTime = measurement duration dt_rng = (ACQTMIN * 1e-3, ACQTMAX * 1e-3) # s self.dwellTime = model.FloatContinuous(1, dt_rng, unit="s") # Indicate first dim is time and second dim is (useless) X (in reversed order) self._metadata[model.MD_DIMS] = "XT" self._shape = (HISTCHAN, 1, 2**16 ) # Histogram is 32 bits, but only return 16 bits info # Set the CFD parameters (in mV) for i, (dv, zc) in enumerate(zip(disc_volt, zero_cross)): self.SetInputCFD(i, int(dv * 1000), int(zc * 1000)) tresbase, bs = self.GetBaseResolution() tres = self.GetResolution() pxd_rng = (tresbase * 1e-12, 2**(BINSTEPSMAX - 1) * tresbase * 1e-12) self.pixelDuration = model.FloatContinuous( tres, pxd_rng, unit="s", setter=self._setPixelDuration) self._metadata[model.MD_PIXEL_DUR] = tres res = self._shape[:2] self.resolution = model.ResolutionVA(res, (res, res), readonly=True) self.syncOffset = model.FloatContinuous( 0, (SYNCOFFSMIN * 1e-12, SYNCOFFSMAX * 1e-12), unit="s", setter=self._setSyncOffset) # Make sure the device is synchronised and metadata is updated self._setSyncOffset(self.syncOffset.value) # Wrapper for the dataflow self.data = BasicDataFlow(self) # Note: Apparently, the hardware supports reading the data, while it's # still accumulating (ie, the acquisition is still running). # We don't support this feature for now, and if the user needs to see # the data building up, it shouldn't be costly (in terms of overhead or # noise) to just do multiple small acquisitions and do the accumulation # in software. # Alternatively, we could provide a second dataflow that sends the data # while it's building up. # Queue to control the acquisition thread: # * "S" to start # * "E" to end # * "T" to terminate self._genmsg = Queue.Queue() self._generator = threading.Thread( target=self._acquire, name="PicoHarp300 acquisition thread") self._generator.start()
def __init__(self, name, role, dependencies, **kwargs): ''' dependencies (dict string->model.HwComponent): the dependencies There must be exactly two dependencies "spectrograph" and "detector". The first dimension of the CCD is supposed to be along the wavelength, with the first pixels representing the lowest wavelengths. Raise: ValueError: if the dependencies are not compatible ''' # we will fill the set of dependencies with Components later in ._dependencies model.Detector.__init__(self, name, role, dependencies=dependencies, **kwargs) # Check the dependencies dt = dependencies["detector"] if not isinstance(dt, ComponentBase): raise ValueError("Dependency detector is not a component.") if ((not hasattr(dt, "shape") or not isinstance(dt.shape, tuple)) or not model.hasVA(dt, "pixelSize")): raise ValueError( "Dependency detector is not a Detector component.") if not hasattr(dt, "data") or not isinstance(dt.data, DataFlowBase): raise ValueError("Dependency detector has not .data DataFlow.") self._detector = dt self.dependencies.value.add(dt) sp = dependencies["spectrograph"] if not isinstance(sp, ComponentBase): raise ValueError("Dependency spectrograph is not a component.") try: if "wavelength" not in sp.axes: raise ValueError( "Dependency spectrograph has no 'wavelength' axis.") except Exception: raise ValueError("Dependency spectrograph is not an Actuator.") self._spectrograph = sp self.dependencies.value.add(sp) # set up the detector part # check that the shape is "horizontal" if dt.shape[0] <= 1: raise ValueError( "Dependency detector must have at least 2 pixels horizontally") if dt.shape[0] < dt.shape[1]: logging.warning( "Dependency detector is shaped vertically (%dx%d), " "this is probably incorrect, as wavelengths are " "expected to be along the horizontal axis", dt.shape[0], dt.shape[1]) # shape is same as detector (raw sensor), but the max resolution is always flat self._shape = tuple(dt.shape) # duplicate # Wrapper for the dataflow self.data = SpecDataFlow(self, dt.data) # The resolution and binning are derived from the detector, but with # settings set so that there is only one horizontal line. # So max vertical resolution is always 1. assert dt.resolution.range[0][1] == 1 resolution = (dt.resolution.range[1][0], 1) # max,1 min_res = (dt.resolution.range[0][0], 1) max_res = (dt.resolution.range[1][0], 1) self.resolution = model.ResolutionVA(resolution, (min_res, max_res), setter=self._setResolution) # The vertical binning is linked to the detector resolution, as it # represents how many pixels are used (and binned). So the maximum # binning is the maximum *resolution* (and it's converted either to # detector binning, or binned later in software). min_bin = (dt.binning.range[0][0], dt.binning.range[0][1]) max_bin = (dt.binning.range[1][0], dt.resolution.range[1][1]) # Initial binning is minimum binning horizontally, and maximum vertically self._binning = (1, dt.resolution.range[1][1]) self._max_det_vbin = dt.binning.range[1][1] self.binning = model.ResolutionVA(self._binning, (min_bin, max_bin), setter=self._setBinning) self._setBinning(self._binning) # will also update the resolution # TODO: also wrap translation, if it exists? # duplicate every other VA and Event from the detector # that includes required VAs like .pixelSize and .exposureTime for aname, value in chain( model.getVAs(dt).items(), model.getEvents(dt).items()): if not hasattr(self, aname): setattr(self, aname, value) else: logging.debug( "skipping duplication of already existing VA '%s'", aname) assert hasattr(self, "pixelSize") if not model.hasVA(self, "exposureTime"): logging.warning("Spectrometer %s has no exposureTime VA", name) sp.position.subscribe(self._onPositionUpdate) self.resolution.subscribe(self._onResBinning) self.binning.subscribe(self._onResBinning) self._updateWavelengthList()
def __init__(self, name, role, mag, mag_choices=None, na=0.95, ri=1, pole_pos=None, x_max=None, hole_diam=None, focus_dist=None, parabola_f=None, rotation=None, **kwargs): """ name (string): should be the name of the product (for metadata) mag (float > 0): magnification ratio mag_choices (None, list of floats > 0): list of allowed magnification ratio. If None, the magnification will be allowed for any value between 1e-3 to 1e6. na (float > 0): numerical aperture ri (0.01 < float < 100): refractive index pole_pos (2 floats > 0): position of the pole on the CCD (in px, without binning, with the top-left pixel as origin). Used for angular resolved imaging on SPARC (only). cf MD_AR_POLE x_max (float): the distance between the parabola origin and the cutoff position (in meters). Used for angular resolved imaging on SPARC (only). cf MD_AR_XMAX hole_diam (float): diameter of the hole in the mirror (in meters). Used for angular resolved imaging on SPARC (only). cf MD_AR_HOLE_DIAMETER focus_dist (float): the vertical mirror cutoff, iow the min distance between the mirror and the sample (in meters). Used for angular resolved imaging on SPARC (only). cf MD_AR_FOCUS_DISTANCE parabola_f (float): parabola_parameter=1/4f. Used for angular resolved imaging on SPARC (only). cf MD_AR_PARABOLA_F rotation (0<float<2*pi): rotation between the Y axis of the SEM referential and the optical path axis. Used on the SPARC to report the rotation between the AR image and the SEM image. """ assert (mag > 0) model.HwComponent.__init__(self, name, role, **kwargs) self._swVersion = "N/A (Odemis %s)" % odemis.__version__ self._hwVersion = name # allow the user to modify the value, if the lens is manually changed if mag_choices is None: self.magnification = model.FloatContinuous(mag, range=(1e-3, 1e6), unit="") else: mag_choices = frozenset(mag_choices) if mag not in mag_choices: raise ValueError("mag (%s) is not within the mag_choices %s" % (mag, mag_choices)) self.magnification = model.FloatEnumerated(mag, choices=mag_choices, unit="") self.numericalAperture = model.FloatContinuous(na, range=(1e-6, 1e3), unit="") self.refractiveIndex = model.FloatContinuous(ri, range=(0.01, 10), unit="") if pole_pos is not None: if (not isinstance(pole_pos, collections.Iterable) or len(pole_pos) != 2 or any(not 0 < v < 1e6 for v in pole_pos)): raise ValueError("pole_pos must be 2 positive values, got %s" % pole_pos) self.polePosition = model.ResolutionVA(tuple(pole_pos), rng=((0, 0), (1e6, 1e6)), unit="px") if x_max is not None: self.xMax = model.FloatVA(x_max, unit="m") if hole_diam is not None: self.holeDiameter = model.FloatVA(hole_diam, unit="m") if focus_dist is not None: self.focusDistance = model.FloatVA(focus_dist, unit="m") if parabola_f is not None: self.parabolaF = model.FloatVA(parabola_f, unit="m") if rotation is not None: self.rotation = model.FloatContinuous(rotation, (0, 2 * math.pi), unit="rad")
def __init__(self, name, role, image, dependencies=None, daemon=None, blur_factor=1e4, max_res=None, **kwargs): """ dependencies (dict string->Component): If "focus" is passed, and it's an actuator with a z axis, the image will be blurred based on the position, to simulate a focus axis. image (str or None): path to a file to use as fake image (relative to the directory of this class) max_res (tuple of (int, int) or None): maximum resolution to clip simulated image, if None whole image shape will be used. The simulated image will be a part of the original image based on the MD_POS metadata. """ # TODO: support transpose? If not, warn that it's not accepted # fake image setup image = str(image) # ensure relative path is from this file if not os.path.isabs(image): image = os.path.join(os.path.dirname(__file__), image) converter = dataio.find_fittest_converter(image, mode=os.O_RDONLY) self._img = converter.read_data(image)[0] # can be RGB or greyscale model.DigitalCamera.__init__(self, name, role, dependencies=dependencies, daemon=daemon, **kwargs) if self._img.ndim > 3: # remove dims of length 1 self._img = numpy.squeeze(self._img) imshp = self._img.shape if len(imshp) == 3 and imshp[0] in {3, 4}: # CYX, change it to YXC, to simulate a RGB detector self._img = numpy.rollaxis(self._img, 2) # XCY self._img = numpy.rollaxis(self._img, 2) # YXC imshp = self._img.shape def clip_max_res(img_res): if len(max_res) != 2: raise ValueError("Shape of max_res should be = 2.") return tuple(min(x, y) for x, y in zip( img_res, max_res)) # in case max_res > image shape # For RGB, the colour is last dim, but we still indicate it as higher # dimension to ensure shape always starts with X, Y if len(imshp) == 3 and imshp[-1] in {3, 4}: # resolution doesn't affect RGB dim res = imshp[-2::-1] self._img_res = res # Original image shape in case it's clipped if max_res: res = clip_max_res(res) self._shape = res + imshp[-1::] # X, Y, C # indicate it's RGB pixel-per-pixel ordered self._img.metadata[model.MD_DIMS] = "YXC" else: self._img_res = imshp[:: -1] # Original image shape in case it's clipped res = imshp[::-1] if max_res is None else tuple(max_res) if max_res: res = clip_max_res(res) self._shape = res # X, Y,... # TODO: handle non integer dtypes depth = 2**(self._img.dtype.itemsize * 8) self._shape += (depth, ) self._resolution = res self.resolution = model.ResolutionVA(self._resolution, ((1, 1), self._resolution), setter=self._setResolution) self._binning = (1, 1) self.binning = model.ResolutionVA(self._binning, ((1, 1), (16, 16)), setter=self._setBinning) hlf_shape = (self._shape[0] // 2 - 1, self._shape[1] // 2 - 1) tran_rng = [(-hlf_shape[0], -hlf_shape[1]), (hlf_shape[0], hlf_shape[1])] self._translation = (0, 0) self.translation = model.ResolutionVA(self._translation, tran_rng, unit="px", cls=(int, long), setter=self._setTranslation) self._orig_exp = self._img.metadata.get(model.MD_EXP_TIME, 0.1) # s self.exposureTime = model.FloatContinuous(self._orig_exp, (1e-3, 10), unit="s") # Some code care about the readout rate to know how long an acquisition will take self.readoutRate = model.FloatVA(1e9, unit="Hz", readonly=True) pxs = self._img.metadata.get(model.MD_PIXEL_SIZE, (10e-6, 10e-6)) mag = self._img.metadata.get(model.MD_LENS_MAG, 1) spxs = tuple(s * mag for s in pxs) self.pixelSize = model.VigilantAttribute(spxs, unit="m", readonly=True) self._metadata = { model.MD_HW_NAME: "FakeCam", model.MD_SENSOR_PIXEL_SIZE: spxs, model.MD_DET_TYPE: model.MD_DT_INTEGRATING } # Set the amount of blurring during defocusing. self._blur_factor = float(blur_factor) try: focuser = dependencies["focus"] if (not isinstance(focuser, model.ComponentBase) or not hasattr(focuser, "axes") or not isinstance(focuser.axes, dict) or "z" not in focuser.axes): raise ValueError( "focus %s must be a Actuator with a 'z' axis" % (focuser, )) self._focus = focuser # The "good" focus is at the current position self._good_focus = self._focus.position.value["z"] self._metadata[model.MD_FAV_POS_ACTIVE] = {"z": self._good_focus} logging.debug("Simulating focus, with good focus at %g m", self._good_focus) except (TypeError, KeyError): logging.info("Will not simulate focus") self._focus = None # Simple implementation of the flow: we keep generating images and if # there are subscribers, they'll receive it. self.data = SimpleDataFlow(self) self._generator = None # Convenience event for the user to connect and fire self.softwareTrigger = model.Event() # Include a thread which creates or fixes an hardware error in the simcam on the basis of the presence of the # file ERROR_STATE_FILE in model.BASE_DIRECTORY self._is_running = True self._error_creation_thread = threading.Thread( target=self._state_error_run, name="Creating and state error") self._error_creation_thread.daemon = True self._error_creation_thread.start()
def __init__(self, name, role, parent, image, spectrograph=None, daemon=None, **kwargs): """ Initializes a fake readout camera. :parameter name: (str) as in Odemis :parameter role: (str) as in Odemis :parameter parent: class streakcamera :parameter image: fake input image """ # TODO image focus and operate mode # get the fake images try: image_filename = str(image) # ensure relative path is from this file if not os.path.isabs(image): image_filename = os.path.join(os.path.dirname(__file__), image) converter = dataio.find_fittest_converter(image_filename, mode=os.O_RDONLY) self._img_list = [] img_list = converter.read_data(image_filename) for img in img_list: if img.ndim > 3: # remove dims of length 1 img = numpy.squeeze(img) self._img_list.append(img) # can be RGB or greyscale except Exception: raise ValueError("Fake image does not fit requirements for temporal spectrum acquisition.") super(ReadoutCamera, self).__init__(name, role, parent=parent, daemon=daemon, **kwargs) # init HwComponent self.parent = parent self._metadata[model.MD_HW_VERSION] = 'Simulated readout camera OrcaFlash 4.0 V3, ' \ 'Product number: C13440-20C, Serial number: 301730' self._metadata[model.MD_SW_VERSION] = 'Firmware: 4.20.B, Version: 4.20.B03-A19-B02-4.02' self._metadata[model.MD_DET_TYPE] = model.MD_DT_INTEGRATING # sensor size (resolution) # x (lambda): horizontal, y (time): vertical full_res = (self._img_list[0].shape[1], self._img_list[0].shape[0]) self._metadata[model.MD_SENSOR_SIZE] = full_res # 16-bit depth = 2 ** (self._img_list[0].dtype.itemsize * 8) self._shape = full_res + (depth,) # variable needed to update resolution VA and wavelength list correctly (_updateWavelengthList()) self._binning = (2, 2) # need to be before binning, as it is modified when changing binning resolution = (int(full_res[0]/self._binning[0]), int(full_res[1]/self._binning[1])) self.resolution = model.ResolutionVA(resolution, ((1, 1), full_res), setter=self._setResolution) # variable needed to update wavelength list correctly (_updateWavelengthList()) self._resolution = self.resolution.value choices_bin = {(1, 1), (2, 2), (4, 4)} self.binning = model.VAEnumerated(self._binning, choices_bin, setter=self._setBinning) self._metadata[model.MD_BINNING] = self.binning.value # physical pixel size is 6.5um x 6.5um sensor_pixelsize = (6.5e-06, 6.5e-06) self._metadata[model.MD_SENSOR_PIXEL_SIZE] = sensor_pixelsize # pixelsize VA is the sensor size, it does not include binning or magnification self.pixelSize = model.VigilantAttribute(sensor_pixelsize, unit="m", readonly=True) range_exp = [0.00001, 1] # 10us to 1s self._exp_time = 0.1 # 100 msec self.exposureTime = model.FloatContinuous(self._exp_time, range_exp, unit="s", setter=self._setCamExpTime) self._metadata[model.MD_EXP_TIME] = self.exposureTime.value self.readoutRate = model.VigilantAttribute(425000000, unit="Hz", readonly=True) # MHz self._metadata[model.MD_READOUT_TIME] = 1 / self.readoutRate.value # s # spectrograph VAs after readout camera VAs self._spectrograph = spectrograph if self._spectrograph: logging.debug("Starting streak camera with spectrograph.") self._spectrograph.position.subscribe(self._updateWavelengthList, init=True) else: logging.warning("No spectrograph specified. No wavelength metadata will be attached.") # for synchronized acquisition self._sync_event = None self.softwareTrigger = model.Event() # Simple implementation of the flow: we keep generating images and if # there are subscribers, they'll receive it. self.data = SimpleStreakCameraDataFlow(self._start, self._stop, self._sync) self._generator = None self._img_counter = 0 # initialize the image counter
def __init__(self, name, role, children, **kwargs): ''' children (dict string->model.HwComponent): the children There must be exactly two children "spectrograph" and "detector". The first dimension of the CCD is supposed to be along the wavelength, with the first pixels representing the lowest wavelengths. Raise an ValueError exception if the children are not compatible ''' # we will fill the set of children with Components later in ._children model.Detector.__init__(self, name, role, **kwargs) # Check the children dt = children["detector"] if not isinstance(dt, ComponentBase): raise ValueError("Child detector is not a component.") if not hasattr(dt, "shape") or not isinstance(dt.shape, tuple): raise ValueError("Child detector is not a Detector component.") if not hasattr(dt, "data") or not isinstance(dt.data, DataFlowBase): raise ValueError("Child detector is not a Detector component.") self._detector = dt self.children.add(dt) sp = children["spectrograph"] if not isinstance(sp, ComponentBase): raise ValueError("Child spectrograph is not a component.") try: if not "wavelength" in sp.axes: raise ValueError( "Child spectrograph has no 'wavelength' axis.") except Exception: raise ValueError("Child spectrograph is not an Actuator.") self._spectrograph = sp self.children.add(sp) # set up the detector part # check that the shape is "horizontal" if dt.shape[0] <= 1: raise ValueError( "Child detector must have at least 2 pixels horizontally") if dt.shape[0] < dt.shape[1]: logging.warning( "Child detector is shaped vertically (%dx%d), " "this is probably incorrect, as wavelengths are " "expected to be along the horizontal axis", dt.shape[0], dt.shape[1]) # shape is same as detector (raw sensor), but the max resolution is always flat self._shape = tuple(dt.shape) # duplicate # The resolution and binning are derived from the detector, but with # settings set so that there is only one horizontal line. # TODO: give a init parameter or VA to specify a smaller window height # than the entire CCD (some spectrometers have only noise on the top and # bottom) if dt.binning.range[1][1] < dt.resolution.range[1][1]: # without software binning, we are stuck to the max binning logging.info( "Spectrometer %s will only use a %d px band of the %d " "px of the sensor", name, dt.binning.range[1][1], dt.resolution.range[1][1]) resolution = [dt.resolution.range[1][0], 1] # max,1 binning = [1, 1] # horizontally: as fine as possible, with a maximum around 256px, over # this, use binning if possible binning[0] = min(max(resolution[0] // 256, dt.binning.range[0][0]), dt.binning.range[1][0]) resolution[0] //= binning[0] # vertically: 1, with binning as big as possible binning[1] = min(dt.binning.range[1][1], dt.resolution.range[1][1]) min_res = (dt.resolution.range[0][0], 1) max_res = (dt.resolution.range[1][0], 1) self.resolution = model.ResolutionVA(resolution, [min_res, max_res], setter=self._setResolution) # 2D binning is like a "small resolution" self._binning = binning self.binning = model.ResolutionVA(self._binning, dt.binning.range, setter=self._setBinning) self._setBinning(binning) # will also update the resolution # TODO: support software binning by rolling up our own dataflow that # does data merging assert dt.resolution.range[0][1] == 1 self.data = dt.data # duplicate every other VA and Event from the detector # that includes required VAs like .pixelSize and .exposureTime for aname, value in model.getVAs(dt).items() + model.getEvents( dt).items(): if not hasattr(self, aname): setattr(self, aname, value) else: logging.debug( "skipping duplication of already existing VA '%s'", aname) assert hasattr(self, "pixelSize") assert hasattr(self, "exposureTime") # Update metadata of detector with wavelength conversion # whenever the wavelength/grating axes moves. try: self._pn_phys = sp.getPolyToWavelength() except AttributeError: raise ValueError( "Child spectrograph has no getPolyToWavelength() method") sp.position.subscribe(self._onPositionUpdate) self.resolution.subscribe(self._onResBinningUpdate) self.binning.subscribe(self._onResBinningUpdate, init=True)
def __init__(self, name, role, parent, aperture=100e-6, wd=10e-3, **kwargs): """ aperture (0 < float): aperture diameter of the electron lens wd (0 < float): working distance """ # It will set up ._shape and .parent model.Emitter.__init__(self, name, role, parent=parent, **kwargs) self._aperture = aperture self._working_distance = wd fake_img = self.parent.fake_img if parent._drift_period: # half the size, to keep some margin for the drift self._shape = tuple(v // 2 for v in fake_img.shape[::-1]) else: self._shape = fake_img.shape[::-1] # next two values are just to determine the pixel size # Distance between borders if magnification = 1. It should be found out # via calibration. We assume that image is square, i.e., VFV = HFV self._hfw_nomag = 0.25 # m # pixelSize is the same as MD_PIXEL_SIZE, with scale == 1 # == smallest size/ between two different ebeam positions pxs = fake_img.metadata[model.MD_PIXEL_SIZE] self.pixelSize = model.VigilantAttribute(pxs, unit="m", readonly=True) # the horizontalFoV VA indicates that it's possible to control the zoom hfv = pxs[0] * self._shape[0] self.horizontalFoV = model.FloatContinuous(hfv, range=[10e-9, 10e-3], unit="m") self.magnification = model.VigilantAttribute(self._hfw_nomag / hfv, unit="", readonly=True) self.horizontalFoV.subscribe(self._onHFV) # To provide some rough idea of the step size when changing focus # Depends on the pixelSize, so will be updated whenever the HFW changes self.depthOfField = model.FloatContinuous(1e-6, range=(0, 1e9), unit="m", readonly=True) self._updateDepthOfField() # needs .pixelSize # (.resolution), .translation, .rotation, and .scaling are used to # define the conversion from coordinates to a region of interest. # (float, float) in m => physically moves the e-beam. shift_rng = ((-50e-06, -50e-06), (50e-06, 50e-06)) self.shift = model.TupleContinuous((0, 0), shift_rng, cls=(int, long, float), unit="m") # (float, float) in m => moves center of acquisition by this amount # independent of scale and rotation. tran_rng = [(-self._shape[0] / 2, -self._shape[1] / 2), (self._shape[0] / 2, self._shape[1] / 2)] self.translation = model.TupleContinuous((0, 0), tran_rng, cls=(int, long, float), unit="px", setter=self._setTranslation) # .resolution is the number of pixels actually scanned. If it's less than # the whole possible area, it's centered. resolution = (self._shape[0] // 4, self._shape[1] // 4) self.resolution = model.ResolutionVA(resolution, [(1, 1), self._shape], setter=self._setResolution) self._resolution = resolution # (float, float) as a ratio => how big is a pixel, compared to pixelSize # it basically works the same as binning, but can be float # (Default to scan the whole area) self._scale = (self._shape[0] / resolution[0], self._shape[1] / resolution[1]) self.scale = model.TupleContinuous(self._scale, [(1, 1), self._shape], cls=(int, long, float), unit="", setter=self._setScale) self.scale.subscribe(self._onScale, init=True) # to update metadata # (float) in rad => rotation of the image compared to the original axes self.rotation = model.FloatContinuous(0, [0, 2 * math.pi], unit="rad") self.dwellTime = model.FloatContinuous(1e-06, (1e-06, 1000), unit="s") # VAs to control the ebeam, purely fake self.probeCurrent = model.FloatEnumerated(1.3e-9, {0.1e-9, 1.3e-9, 2.6e-9, 3.4e-9, 11.564e-9, 23e-9}, unit="A") self.accelVoltage = model.FloatContinuous(10e3, (1e3, 30e3), unit="V") # Pretend it's ready to acquire an image self.power = model.BooleanVA(True) # Blanker has a None = "auto" mode which automatically blanks when not scanning self.blanker = model.VAEnumerated(None, choices={True: 'blanked', False: 'unblanked', None: 'auto'})
def __init__(self, name, role, parent, fov_range, **kwargs): # It will set up ._shape and .parent model.Emitter.__init__(self, name, role, parent=parent, **kwargs) self._shape = (2048, 2048) # This is the field of view when in Tescan Software magnification = 100 # and working distance = 0,27 m (maximum WD of Mira TC). When working # distance is changed (for example when we focus) magnification mention # in odemis and Tescan software are expected to be different. self._hfw_nomag = 0.195565 # m # Get current field of view and compute magnification fov = self.parent._device.GetViewField() * 1e-03 mag = self._hfw_nomag / fov # Field of view in Tescan is set in mm self.parent._device.SetViewField(self._hfw_nomag * 1e03 / mag) self.magnification = model.VigilantAttribute(mag, unit="", readonly=True) self.horizontalFOV = model.FloatContinuous( fov, range=fov_range, unit="m", setter=self._setHorizontalFOV) self.horizontalFOV.subscribe( self._onHorizontalFOV) # to update metadata # pixelSize is the same as MD_PIXEL_SIZE, with scale == 1 # == smallest size/ between two different ebeam positions pxs = (self._hfw_nomag / (self._shape[0] * mag), self._hfw_nomag / (self._shape[1] * mag)) self.pixelSize = model.VigilantAttribute(pxs, unit="m", readonly=True) # (.resolution), .translation, .rotation, and .scaling are used to # define the conversion from coordinates to a region of interest. # (float, float) in px => moves center of acquisition by this amount # independent of scale and rotation. tran_rng = [(-self._shape[0] / 2, -self._shape[1] / 2), (self._shape[0] / 2, self._shape[1] / 2)] self.translation = model.TupleContinuous((0, 0), tran_rng, cls=(int, long, float), unit="", setter=self._setTranslation) # .resolution is the number of pixels actually scanned. If it's less than # the whole possible area, it's centered. resolution = (self._shape[0] // 8, self._shape[1] // 8) self.resolution = model.ResolutionVA(resolution, [(1, 1), self._shape], setter=self._setResolution) self._resolution = resolution # (float, float) as a ratio => how big is a pixel, compared to pixelSize # it basically works the same as binning, but can be float # (Default to scan the whole area) self._scale = (self._shape[0] / resolution[0], self._shape[1] / resolution[1]) self.scale = model.TupleContinuous(self._scale, [(1, 1), self._shape], cls=(int, long, float), unit="", setter=self._setScale) self.scale.subscribe(self._onScale, init=True) # to update metadata # (float) in rad => rotation of the image compared to the original axes # TODO: for now it's readonly because no rotation is supported self.rotation = model.FloatContinuous(0, [0, 2 * math.pi], unit="rad", readonly=True) self.dwellTime = model.FloatContinuous(1e-06, (1e-06, 1000), unit="s") self.dwellTime.subscribe(self._onDwellTime) # Range is according to min and max voltages accepted by Tescan API volt_range = self.GetVoltagesRange() volt = self.parent._device.HVGetVoltage() self.accelVoltage = model.FloatContinuous(volt, volt_range, unit="V") self.accelVoltage.subscribe(self._onVoltage) # 0 turns off the e-beam, 1 turns it on power_choices = set([0, 1]) self._power = self.parent._device.HVGetBeam() # Don't change state self.power = model.IntEnumerated(self._power, power_choices, unit="", setter=self._setPower) # Blanker is automatically enabled when no scanning takes place # TODO it may cause time overhead, check on testing => If so put some # small timeout (~ a few seconds) before blanking the beam. # self.parent._device.ScSetBlanker(0, 2) # Enumerated float with respect to the PC indexes of Tescan API self._list_currents = self.GetProbeCurrents() pc_choices = set(self._list_currents) # We use the current PC self._probeCurrent = self._list_currents[ self.parent._device.GetPCIndex() - 1] self.probeCurrent = model.FloatEnumerated(self._probeCurrent, pc_choices, unit="A", setter=self._setPC)
def __init__(self, name, role, sn=None, **kwargs): """ sn (string or None): serial number of the device to open. If None, it will pick the first device found. If "fake", it will use a simulated device. """ super(Spectrometer, self).__init__(name, role, **kwargs) if sn == "fake": self._dll = FakeAvantesDLL() sn = None else: self._dll = AvantesDLL() # Look for the spectrometer and initialize it self._dev_id, self._dev_hdl = self._open_device(sn) fpga_ver, fw_ver, lib_ver = self.GetVersionInfo() config = self.GetParameter() sensor = config.Detector.SensorType sensor_name = SensorTypes.get(sensor, str(sensor)) self._swVersion = "libavs v%s" % (lib_ver,) # Reported UserFriendlyName is the same as SerialNumber self._hwVersion = ("AvaSpec sensor %s (s/n %s) FPGA v%s, FW v%s " % (sensor_name, self._dev_id.SerialNumber.decode("ascii"), fpga_ver, fw_ver)) self._metadata[model.MD_HW_VERSION] = self._hwVersion self._metadata[model.MD_SW_VERSION] = self._swVersion # Note: It seems that by default it uses the maximum cooling temperature. # so that's good enough for us. We could try to change it with config.TecControl.Setpoint. npixels = self.GetNumPixels() # TODO: are there drawbacks in using it in 16-bits? The demo always set it # to 16-bits. Is that just for compatibility with old hardware? self.UseHighResAdc(True) # Default is 14 bits # Intensity is in float, but range is based on uint16 self._shape = (npixels, 1, float(2 ** 16)) # The hardware light diffraction is fixed, and there is no support for # binning, and we don't accept cropping, so the wavelength is completely fixed. self._metadata[model.MD_WL_LIST] = list(self.GetLambda(npixels) * 1e-9) # Indicate the data contains spectrum on the "fast" dimension self._metadata[model.MD_DIMS] = "XC" self.exposureTime = model.FloatContinuous(1, INTEGRATION_TIME_RNG, unit="s", setter=self._onExposureTime) # Not so useful, but makes happy some client when trying to estimate the # acquisition time. Not sure whether this is correct, but it's good enough self.readoutRate = model.VigilantAttribute(CLOCK_RATE, readonly=True, unit="Hz") # No support for binning/resolution change, but we put them, as it helps # to follow the standard interface, so there rest of Odemis is happy self.binning = model.ResolutionVA((1, 1), ((1, 1), (1, 1))) self.resolution = model.ResolutionVA((npixels, 1), ((npixels, 1), (npixels, 1))) self.data = AvantesDataFlow(self) self.softwareTrigger = model.Event() # Queue to control the acquisition thread self._genmsg = queue.Queue() # GEN_* # Queue of all synchronization events received (typically max len 1) self._old_triggers = [] self._data_ready = threading.Event() # set when new data is available # Thread of the generator self._generator = None
def __init__(self, name, role, array, **kwargs): super(StaticCCD, self).__init__(name, role, **kwargs) self.array = array self.data = StaticImageDataFlow(self) res = self.array.shape self.resolution = model.ResolutionVA(res, (res, res), readonly=True)
def __init__(self, name, role, image, dependencies=None, daemon=None, blur_factor=1e4, resolution=None, **kwargs): ''' dependencies (dict string->Component): If "focus" is passed, and it's an actuator with a z axis, the image will be blurred based on the position, to simulate a focus axis. image (str or None): path to a file to use as fake image (relative to the directory of this class) ''' # TODO: support transpose? If not, warn that it's not accepted # fake image setup image = unicode(image) # ensure relative path is from this file if not os.path.isabs(image): image = os.path.join(os.path.dirname(__file__), image) converter = dataio.find_fittest_converter(image, mode=os.O_RDONLY) self._imgs = converter.read_data(image) # can be RGB or greyscale model.DigitalCamera.__init__(self, name, role, dependencies=dependencies, daemon=daemon, **kwargs) for i, img in enumerate(self._imgs): if img.ndim > 3: # remove dims of length 1 self._imgs[i] = numpy.squeeze(img) imshp = img.shape if len(imshp) == 3 and imshp[i] in {3, 4}: # CYX, change it to YXC, to simulate a RGB detector self._imgs[i] = util.img.ensureYXC(img) for img in self._imgs[1:]: if self._imgs[0].shape != self._imgs[i].shape: raise ValueError("all images must have the same resolution") imshp = self._imgs[0].shape # For RGB, the colour is last dim, but we still indicate it as higher # dimension to ensure shape always starts with X, Y if len(imshp) == 3 and imshp[-1] in {3, 4}: # resolution doesn't affect RGB dim if resolution: if resolution >= imshp[-2::-1]: res = tuple(resolution) else: res = imshp[-2::-1] self._shape = res + imshp[-1::] # X, Y, C else: if resolution: res = tuple(resolution) else: res = imshp[::-1] self._shape = res # X, Y,... # TODO: handle non integer dtypes depth = 2**(self._imgs[0].dtype.itemsize * 8) self._shape += (depth, ) self._resolution = res self.resolution = model.ResolutionVA(self._resolution, ((1, 1), self._resolution), setter=self._setResolution) self._binning = (1, 1) self.binning = model.ResolutionVA(self._binning, ((1, 1), (16, 16)), setter=self._setBinning) hlf_shape = (self._shape[0] // 2 - 1, self._shape[1] // 2 - 1) tran_rng = [(-hlf_shape[0], -hlf_shape[1]), (hlf_shape[0], hlf_shape[1])] self._translation = (0, 0) self.translation = model.ResolutionVA(self._translation, tran_rng, cls=(int, long), unit="px", setter=self._setTranslation) exp = self._imgs[0].metadata.get(model.MD_EXP_TIME, 0.1) # s self.exposureTime = model.FloatContinuous(exp, (1e-3, 1e3), unit="s") # Some code care about the readout rate to know how long an acquisition will take self.readoutRate = model.FloatVA(1e9, unit="Hz", readonly=True) pxs = self._imgs[0].metadata.get(model.MD_PIXEL_SIZE, (10e-6, 10e-6)) mag = self._imgs[0].metadata.get(model.MD_LENS_MAG, 1) spxs = tuple(s * mag for s in pxs) self.pixelSize = model.VigilantAttribute(spxs, unit="m", readonly=True) self._metadata = { model.MD_HW_NAME: "FakeCam", model.MD_SENSOR_PIXEL_SIZE: spxs, model.MD_DET_TYPE: model.MD_DT_INTEGRATING, model.MD_PIXEL_SIZE: pxs } # Set the amount of blurring during defocusing. self._blur_factor = float(blur_factor) try: focuser = dependencies["focus"] if (not isinstance(focuser, model.ComponentBase) or not hasattr(focuser, "axes") or not isinstance(focuser.axes, dict) or "z" not in focuser.axes): raise ValueError( "focus %s must be a Actuator with a 'z' axis" % (focuser, )) self._focus = focuser # The "good" focus is at the current position self._good_focus = self._focus.position.value["z"] self._metadata[model.MD_FAV_POS_ACTIVE] = {"z": self._good_focus} logging.debug("Simulating focus, with good focus at %g m", self._good_focus) except (TypeError, KeyError): logging.info("Will not simulate focus") self._focus = None try: stage = dependencies["stage"] if (not isinstance(stage, model.ComponentBase) or not hasattr(stage, "axes") or not isinstance(stage.axes, dict)): raise ValueError("stage %s must be a Actuator with a 'z' axis", stage) self._stage = stage if resolution == None: raise ValueError("resolution is %s", resolution) # the position of the center of the image self._orig_stage_pos = self._stage.position.value[ "x"], self._stage.position.value["y"] logging.debug("Simulating stage at %s m", self._orig_stage_pos) except (TypeError, KeyError): logging.info("Will not simulate stage") self._stage = None # Simple implementation of the flow: we keep generating images and if # there are subscribers, they'll receive it. self.data = SimpleDataFlow(self) self._generator = None # Convenience event for the user to connect and fire self.softwareTrigger = model.Event()
def __init__(self, name, detector, dataflow, emitter, **kwargs): """ :param detector: (DigitalCamera) The optical detector (ccd). :param dataflow: (DataFlow) The dataflow of the detector. :param emitter: (Emitter) The component that generates energy and also controls the position of the energy (the e-beam of the SEM). """ if "acq_type" not in kwargs: kwargs["acq_type"] = model.MD_AT_CL # Skip the RepetitionStream.__init__ because it gets confused with pixelSize being # two floats. acqstream.LiveStream.__init__(self, name, detector, dataflow, emitter) self._scanner = emitter # Region of acquisition (ROI) + repetition is sufficient, but pixel size is nicer for the user. # As the settings are over-specified, whenever ROI, repetition, or pixel # size changes, one (or more) other VA is updated to keep everything # consistent. In addition, there are also hardware constraints, which # must also be satisfied. The main rules followed are: # * Try to keep the VA which was changed (by the user) as close as # possible to the requested value (within hardware limits). # So in practice, the three setters behave in this way: # * region of acquisition set: ROI (as requested) + repetition (current) → PxS (updated) # * pixel size set: PxS (as requested) + ROI (current) → repetition (updated) # The ROA is adjusted to ensure the repetition is a round number and acceptable by the hardware. # * repetition set: Rep (as requested) + ROI (current) → PxS (updated) # The repetition is adjusted to fit the hardware limits # Region of interest as left, top, right, bottom (in ratio from the # whole area of the emitter => between 0 and 1) # We overwrite the VA provided by LiveStream to define a setter. self.roi = model.TupleContinuous((0, 0, 1, 1), range=((0, 0, 0, 0), (1, 1, 1, 1)), cls=(int, float), setter=self._setROI) # Start with pixel size to fit 1024 px, as it's typically a sane value # for the user (and adjust for the hardware). spxs = emitter.pixelSize.value # m, size at scale = 1 sshape = emitter.shape # px, max number of pixels scanned phy_size_x = spxs[0] * sshape[0] # m phy_size_y = spxs[1] * sshape[1] # m pxs = (phy_size_x / 1024, phy_size_y / 1024) roi, rep, pxs = self._updateROIAndPixelSize(self.roi.value, pxs) # The number of pixels acquired in each dimension. It will be assigned to the resolution # of the emitter (but cannot be directly set, as one might want to use the emitter while # configuring the stream). self.repetition = model.ResolutionVA(rep, emitter.resolution.range, setter=self._setRepetition) # The size of the pixel (IOW, the distance between the center of two # consecutive pixels or the "pitch"). Value can vary for vertical and horizontal direction. # The actual range is dynamic, as it changes with the magnification. self.pixelSize = model.TupleContinuous(pxs, range=((0, 0), (1, 1)), unit="m", cls=(int, float), setter=self._setPixelSize) # Typical user wants density much lower than SEM. self.pixelSize.value = tuple(numpy.array(self.pixelSize.value) * 50) # Maximum margin is half the CCD FoV. ccd_rect = get_ccd_fov(detector) max_margin = max(ccd_rect[2] - ccd_rect[0], ccd_rect[3] - ccd_rect[1]) / 2 # roi_margin (0 <= float): extra margin (in m) around the SEM area to select the CCD ROI. self.roi_margin = model.FloatContinuous(0, (0, max_margin), unit="m") # Exposure time of each pixel is the exposure time of the detector. # The dwell time of the emitter will be adapted before the acquisition. # Update the pixel size whenever SEM magnification changes. # This allows to keep the ROI at the same place in the SEM FoV. # Note: This is to be done only if the user needs to manually update the magnification. self.magnification = self._scanner.magnification self._prev_mag = self.magnification.value self.magnification.subscribe(self._onMagnification)
def __init__(self, name, role, children, **kwargs): ''' children (dict string->model.HwComponent): the children There must be exactly two children "spectrograph" and "detector". The first dimension of the CCD is supposed to be along the wavelength, with the first pixels representing the lowest wavelengths. Raise: ValueError: if the children are not compatible ''' # we will fill the set of children with Components later in ._children model.Detector.__init__(self, name, role, **kwargs) # Check the children dt = children["detector"] if not isinstance(dt, ComponentBase): raise ValueError("Child detector is not a component.") if ((not hasattr(dt, "shape") or not isinstance(dt.shape, tuple)) or not model.hasVA(dt, "pixelSize")): raise ValueError("Child detector is not a Detector component.") if not hasattr(dt, "data") or not isinstance(dt.data, DataFlowBase): raise ValueError("Child detector has not .data DataFlow.") self._detector = dt self.children.value.add(dt) sp = children["spectrograph"] if not isinstance(sp, ComponentBase): raise ValueError("Child spectrograph is not a component.") try: if "wavelength" not in sp.axes: raise ValueError("Child spectrograph has no 'wavelength' axis.") except Exception: raise ValueError("Child spectrograph is not an Actuator.") self._spectrograph = sp self.children.value.add(sp) # set up the detector part # check that the shape is "horizontal" if dt.shape[0] <= 1: raise ValueError("Child detector must have at least 2 pixels horizontally") if dt.shape[0] < dt.shape[1]: logging.warning("Child detector is shaped vertically (%dx%d), " "this is probably incorrect, as wavelengths are " "expected to be along the horizontal axis", dt.shape[0], dt.shape[1]) # shape is same as detector (raw sensor), but the max resolution is always flat self._shape = tuple(dt.shape) # duplicate # Wrapper for the dataflow self.data = SpecDataFlow(self, dt.data) # The resolution and binning are derived from the detector, but with # settings set so that there is only one horizontal line. # TODO: give a init parameter or VA to specify a smaller window height # than the entire CCD (some spectrometers have only noise on the top and # bottom) if dt.binning.range[1][1] < dt.resolution.range[1][1]: # without software binning, we are stuck to the max binning # TODO: support software binning by rolling up our own dataflow that # does data merging logging.info("Spectrometer %s will only use a %d px band of the %d " "px of the sensor", name, dt.binning.range[1][1], dt.resolution.range[1][1]) assert dt.resolution.range[0][1] == 1 resolution = (dt.resolution.range[1][0], 1) # max,1 min_res = (dt.resolution.range[0][0], 1) max_res = (dt.resolution.range[1][0], 1) self.resolution = model.ResolutionVA(resolution, (min_res, max_res), setter=self._setResolution) # 2D binning is like a "small resolution" # Initial binning is minimum binning horizontally, and maximum vertically self._binning = (1, min(dt.binning.range[1][1], dt.resolution.range[1][1])) self.binning = model.ResolutionVA(self._binning, dt.binning.range, setter=self._setBinning) self._setBinning(self._binning) # will also update the resolution # TODO: also wrap translation, if it exists? # duplicate every other VA and Event from the detector # that includes required VAs like .pixelSize and .exposureTime for aname, value in model.getVAs(dt).items() + model.getEvents(dt).items(): if not hasattr(self, aname): setattr(self, aname, value) else: logging.debug("skipping duplication of already existing VA '%s'", aname) assert hasattr(self, "pixelSize") if not model.hasVA(self, "exposureTime"): logging.warning("Spectrometer %s has no exposureTime VA", name) sp.position.subscribe(self._onPositionUpdate) self.resolution.subscribe(self._onResBinning) self.binning.subscribe(self._onResBinning) self._updateWavelengthList()
def __init__(self, name, role, mag, mag_choices=None, na=0.95, ri=1, pole_pos=None, x_max=None, hole_diam=None, focus_dist=None, parabola_f=None, rotation=None, configurations=None, **kwargs): """ name (string): should be the name of the product (for metadata) mag (float > 0): magnification ratio mag_choices (None, list of floats > 0): list of allowed magnification ratio. If None, the magnification will be allowed for any value between 1e-3 to 1e6. na (float > 0): numerical aperture ri (0.01 < float < 100): refractive index pole_pos (2 floats > 0): position of the pole on the CCD (in px, without binning, with the top-left pixel as origin). Used for angular resolved imaging on SPARC (only). cf MD_AR_POLE x_max (float): the distance between the parabola origin and the cutoff position (in meters). Used for angular resolved imaging on SPARC (only). cf MD_AR_XMAX hole_diam (float): diameter of the hole in the mirror (in meters). Used for angular resolved imaging on SPARC (only). cf MD_AR_HOLE_DIAMETER focus_dist (float): the vertical mirror cutoff, iow the min distance between the mirror and the sample (in meters). Used for angular resolved imaging on SPARC (only). cf MD_AR_FOCUS_DISTANCE parabola_f (float): parabola_parameter=1/4f. Used for angular resolved imaging on SPARC (only). cf MD_AR_PARABOLA_F rotation (0<float<2*pi): rotation between the Y axis of the SEM referential and the optical path axis. Used on the SPARC to report the rotation between the AR image and the SEM image. configurations (dict str -> (dict str -> value)): {configuration name -> {attribute name -> value}} All the configurations supported and their settings. A "configuration" is a set of attributes with predefined values. When this argument is specified, a .configuration attribute will be available, with each configuration name, and changing it will automatically set all the associated attributes to their predefined value. """ assert (mag > 0) model.HwComponent.__init__(self, name, role, **kwargs) self._swVersion = "N/A (Odemis %s)" % odemis.__version__ self._hwVersion = name # allow the user to modify the value, if the lens is manually changed if mag_choices is None: self.magnification = model.FloatContinuous(mag, range=(1e-3, 1e6), unit="") else: mag_choices = frozenset(mag_choices) if mag not in mag_choices: raise ValueError("mag (%s) is not within the mag_choices %s" % (mag, mag_choices)) self.magnification = model.FloatEnumerated(mag, choices=mag_choices, unit="") self.numericalAperture = model.FloatContinuous(na, range=(1e-6, 1e3), unit="") self.refractiveIndex = model.FloatContinuous(ri, range=(0.01, 10), unit="") if pole_pos is not None: if (not isinstance(pole_pos, collections.Iterable) or len(pole_pos) != 2 or any(not 0 < v < 1e6 for v in pole_pos)): raise ValueError("pole_pos must be 2 positive values, got %s" % pole_pos) self.polePosition = model.ResolutionVA(tuple(pole_pos), rng=((0, 0), (1e6, 1e6)), unit="px") if x_max is not None: self.xMax = model.FloatVA(x_max, unit="m") if hole_diam is not None: self.holeDiameter = model.FloatVA(hole_diam, unit="m") if focus_dist is not None: self.focusDistance = model.FloatVA(focus_dist, unit="m") if parabola_f is not None: self.parabolaF = model.FloatVA(parabola_f, unit="m") if rotation is not None: self.rotation = model.FloatContinuous(rotation, (0, 2 * math.pi), unit="rad") if configurations is not None: self._configurations = configurations # Find the configuration which is closest to the current settings def _compare_config(cn): settings = configurations[cn] score = 0 for arg, value in settings.items(): try: vaname = CONFIG_2_VA[arg] va = getattr(self, vaname) except (KeyError, AttributeError): raise ValueError( "Attribute name predefined in the configuration required" ) if value == va.value: score += 1 return score current_conf = max(configurations, key=_compare_config) self.configuration = model.StringEnumerated( current_conf, choices=set(configurations.keys()), setter=self._setConfiguration)
def __init__(self, name, role, device=None, dependencies=None, children=None, daemon=None, disc_volt=None, zero_cross=None, shutter_axes=None, **kwargs): """ device (None or str): serial number (eg, 1020345) of the device to use or None if any device is fine. dependencies (dict str -> Component): shutters components (shutter0 and shutter1 are valid) children (dict str -> kwargs): the names of the detectors (detector0 and detector1 are valid) disc_volt (2 (0 <= float <= 0.8)): discriminator voltage for the APD 0 and 1 (in V) zero_cross (2 (0 <= float <= 2e-3)): zero cross voltage for the APD0 and 1 (in V) shutter_axes (dict str -> str, value, value): internal child role of the photo-detector -> axis name, position when shutter is closed (ie protected), position when opened (receiving light). """ if dependencies is None: dependencies = {} if children is None: children = {} if device == "fake": device = None self._dll = FakePHDLL() else: self._dll = PHDLL() self._idx = self._openDevice(device) # Lock to be taken to avoid multi-threaded access to the hardware self._hw_access = threading.Lock() if disc_volt is None: disc_volt = [0, 0] if zero_cross is None: zero_cross = [0, 0] super(PH300, self).__init__(name, role, daemon=daemon, dependencies=dependencies, **kwargs) # TODO: metadata for indicating the range? cf WL_LIST? # TODO: do we need TTTR mode? self.Initialise(MODE_HIST) self._swVersion = self.GetLibraryVersion() self._metadata[model.MD_SW_VERSION] = self._swVersion mod, partnum, ver = self.GetHardwareInfo() sn = self.GetSerialNumber() self._hwVersion = "%s %s %s (s/n %s)" % (mod, partnum, ver, sn) self._metadata[model.MD_HW_VERSION] = self._hwVersion self._metadata[model.MD_DET_TYPE] = model.MD_DT_NORMAL logging.info("Opened device %d (%s s/n %s)", self._idx, mod, sn) self.Calibrate() # TODO: needs to be changeable? self.SetOffset(0) # To pass the raw count of each detector, we create children detectors. # It could also go into just separate DataFlow, but then it's difficult # to allow using these DataFlows in a standard way. self._detectors = {} self._shutters = {} self._shutter_axes = shutter_axes or {} for name, ckwargs in children.items(): if name == "detector0": if "shutter0" in dependencies: shutter_name = "shutter0" else: shutter_name = None self._detectors[name] = PH300RawDetector(channel=0, parent=self, shutter_name=shutter_name, daemon=daemon, **ckwargs) self.children.value.add(self._detectors[name]) elif name == "detector1": if "shutter1" in dependencies: shutter_name = "shutter1" else: shutter_name = None self._detectors[name] = PH300RawDetector(channel=1, parent=self, shutter_name=shutter_name, daemon=daemon, **ckwargs) self.children.value.add(self._detectors[name]) else: raise ValueError("Child %s not recognized, should be detector0 or detector1.") for name, comp in dependencies.items(): if name == "shutter0": if "shutter0" not in shutter_axes.keys(): raise ValueError("'shutter0' not found in shutter_axes") self._shutters['shutter0'] = comp elif name == "shutter1": if "shutter1" not in shutter_axes.keys(): raise ValueError("'shutter1' not found in shutter_axes") self._shutters['shutter1'] = comp else: raise ValueError("Dependency %s not recognized, should be shutter0 or shutter1.") # dwellTime = measurement duration dt_rng = (ACQTMIN * 1e-3, ACQTMAX * 1e-3) # s self.dwellTime = model.FloatContinuous(1, dt_rng, unit="s") # Indicate first dim is time and second dim is (useless) X (in reversed order) self._metadata[model.MD_DIMS] = "XT" self._shape = (HISTCHAN, 1, 2**16) # Histogram is 32 bits, but only return 16 bits info # Set the CFD parameters (in mV) for i, (dv, zc) in enumerate(zip(disc_volt, zero_cross)): self.SetInputCFD(i, int(dv * 1000), int(zc * 1000)) tresbase, bs = self.GetBaseResolution() tres = self.GetResolution() pxd_ch = {2 ** i * tresbase * 1e-12 for i in range(BINSTEPSMAX)} self.pixelDuration = model.FloatEnumerated(tres * 1e-12, pxd_ch, unit="s", setter=self._setPixelDuration) res = self._shape[:2] self.resolution = model.ResolutionVA(res, (res, res), readonly=True) self.syncDiv = model.IntEnumerated(1, choices={1, 2, 4, 8}, unit="", setter=self._setSyncDiv) self._setSyncDiv(self.syncDiv.value) self.syncOffset = model.FloatContinuous(0, (SYNCOFFSMIN * 1e-12, SYNCOFFSMAX * 1e-12), unit="s", setter=self._setSyncOffset) # Make sure the device is synchronised and metadata is updated self._setSyncOffset(self.syncOffset.value) # Wrapper for the dataflow self.data = BasicDataFlow(self) # Note: Apparently, the hardware supports reading the data, while it's # still accumulating (ie, the acquisition is still running). # We don't support this feature for now, and if the user needs to see # the data building up, it shouldn't be costly (in terms of overhead or # noise) to just do multiple small acquisitions and do the accumulation # in software. # Alternatively, we could provide a second dataflow that sends the data # while it's building up. # Queue to control the acquisition thread: # * "S" to start # * "E" to end # * "T" to terminate self._genmsg = queue.Queue() self._generator = threading.Thread(target=self._acquire, name="PicoHarp300 acquisition thread") self._generator.start()
def __init__(self, name, role, image, children=None, daemon=None, **kwargs): ''' children (dict string->kwargs): parameters setting for the children. The only possible child is "focus". They will be provided back in the .children VA image (str or None): path to a file to use as fake image (relative to the directory of this class) ''' # TODO: support transpose? If not, warn that it's not accepted # fake image setup image = unicode(image) # ensure relative path is from this file if not os.path.isabs(image): image = os.path.join(os.path.dirname(__file__), image) converter = dataio.find_fittest_converter(image, mode=os.O_RDONLY) self._img = converter.read_data(image)[0] # can be RGB or greyscale # we will fill the set of children with Components later in ._children model.DigitalCamera.__init__(self, name, role, daemon=daemon, **kwargs) if self._img.ndim > 3: # remove dims of length 1 self._img = numpy.squeeze(self._img) imshp = self._img.shape if len(imshp) == 3 and imshp[0] in {3, 4}: # CYX, change it to YXC, to simulate a RGB detector self._img = numpy.rollaxis(self._img, 2) # XCY self._img = numpy.rollaxis(self._img, 2) # YXC imshp = self._img.shape # For RGB, the colour is last dim, but we still indicate it as higher # dimension to ensure shape always starts with X, Y if len(imshp) == 3 and imshp[-1] in {3, 4}: # resolution doesn't affect RGB dim res = imshp[-2::-1] self._shape = res + imshp[-1::] # X, Y, C # indicate it's RGB pixel-per-pixel ordered self._img.metadata[model.MD_DIMS] = "YXC" else: res = imshp[::-1] self._shape = res # X, Y,... # TODO: handle non integer dtypes depth = 2 ** (self._img.dtype.itemsize * 8) self._shape += (depth,) self._resolution = res self.resolution = model.ResolutionVA(self._resolution, ((1, 1), self._resolution), setter=self._setResolution) self._binning = (1, 1) self.binning = model.ResolutionVA(self._binning, ((1, 1), (16, 16)), setter=self._setBinning) hlf_shape = (self._shape[0] // 2 - 1, self._shape[1] // 2 - 1) tran_rng = [(-hlf_shape[0], -hlf_shape[1]), (hlf_shape[0], hlf_shape[1])] self._translation = (0, 0) self.translation = model.ResolutionVA(self._translation, tran_rng, cls=(int, long), unit="px", setter=self._setTranslation) exp = self._img.metadata.get(model.MD_EXP_TIME, 0.1) # s self.exposureTime = model.FloatContinuous(exp, (1e-3, 1e3), unit="s") # Some code care about the readout rate to know how long an acquisition will take self.readoutRate = model.FloatVA(1e9, unit="Hz", readonly=True) pxs = self._img.metadata.get(model.MD_PIXEL_SIZE, (10e-6, 10e-6)) mag = self._img.metadata.get(model.MD_LENS_MAG, 1) spxs = tuple(s * mag for s in pxs) self.pixelSize = model.VigilantAttribute(spxs, unit="m", readonly=True) self._metadata = {model.MD_HW_NAME: "FakeCam", model.MD_SENSOR_PIXEL_SIZE: spxs, model.MD_DET_TYPE: model.MD_DT_INTEGRATING} try: kwargs = children["focus"] except (KeyError, TypeError): logging.info("Will not simulate focus") self._focus = None else: self._focus = CamFocus(parent=self, daemon=daemon, **kwargs) self.children.value = self.children.value | {self._focus} # Simple implementation of the flow: we keep generating images and if # there are subscribers, they'll receive it. self.data = SimpleDataFlow(self) self._generator = None # Convenience event for the user to connect and fire self.softwareTrigger = model.Event()
def __init__(self, name, role, image, children=None, daemon=None, **kwargs): ''' children (dict string->kwargs): parameters setting for the children. The only possible child is "focus". They will be provided back in the .children VA image (str or None): path to a file to use as fake image (relative to the directory of this class) ''' # TODO: support transpose? If not, warn that it's not accepted # fake image setup image = unicode(image) # change to this directory to ensure relative path is from this file os.chdir(os.path.dirname(unicode(__file__))) exporter = dataio.find_fittest_exporter(image) self._img = exporter.read_data(image)[0] # can be RGB or greyscale # we will fill the set of children with Components later in ._children model.DigitalCamera.__init__(self, name, role, daemon=daemon, **kwargs) if self._img.ndim > 3: # remove dims of length 1 self._img = numpy.squeeze(self._img) imshp = self._img.shape if len(imshp) == 3 and imshp[0] in {3, 4}: # CYX, change it to YXC, to simulate a RGB detector self._img = numpy.rollaxis(self._img, 2) # XCY self._img = numpy.rollaxis(self._img, 2) # YXC imshp = self._img.shape # For RGB, the colour is last dim, but we still indicate it as higher # dimension to ensure shape always starts with X, Y if len(imshp) == 3 and imshp[-1] in {3, 4}: # resolution doesn't affect RGB dim res = imshp[-2::-1] self._shape = res + imshp[-1::] # X, Y, C # indicate it's RGB pixel-per-pixel ordered self._img.metadata[model.MD_DIMS] = "YXC" else: res = imshp[::-1] self._shape = res # X, Y,... # TODO: handle non integer dtypes depth = 2**(self._img.dtype.itemsize * 8) self._shape += (depth, ) # TODO: don't provide range? or don't make it readonly? self.resolution = model.ResolutionVA(res, [res, res]) # , readonly=True) # TODO: support (simulated) binning self.binning = model.ResolutionVA((1, 1), [(1, 1), (1, 1)]) exp = self._img.metadata.get(model.MD_EXP_TIME, 0.1) # s self.exposureTime = model.FloatContinuous(exp, [1e-3, 1e3], unit="s") # Some code care about the readout rate to know how long an acquisition will take self.readoutRate = model.FloatVA(1e9, unit="Hz", readonly=True) pxs = self._img.metadata.get(model.MD_PIXEL_SIZE, (10e-6, 10e-6)) mag = self._img.metadata.get(model.MD_LENS_MAG, 1) spxs = tuple(s * mag for s in pxs) self.pixelSize = model.VigilantAttribute(spxs, unit="m", readonly=True) self._metadata = { model.MD_HW_NAME: "FakeCam", model.MD_SENSOR_PIXEL_SIZE: spxs } try: kwargs = children["focus"] except (KeyError, TypeError): logging.info("Will not simulate focus") self._focus = None else: self._focus = CamFocus(parent=self, daemon=daemon, **kwargs) self.children.value = self.children.value | {self._focus} # Simple implementation of the flow: we keep generating images and if # there are subscribers, they'll receive it. self.data = model.DataFlow(self) self._generator = util.RepeatingTimer(exp, self._generate, "SimCam image generator") self._generator.start()