def test_tc(self): """ TupleContinuous """ va = model.TupleContinuous((0.1, 10, .5), ((-1.3, 9, 0), (100., 150., 1.)), cls=(int, long, float)) self.assertEqual(va.value, (0.1, 10, .5)) self.assertEqual(va.range, ((-1.3, 9, 0), (100., 150., 1.))) # must convert anything to a tuple va.value = [-1, 150, .5] self.assertEqual(va.value, (-1, 150, .5)) # must not accept values outside of the range try: va.value = (-1., 160., .5) self.fail("Assigning value not in range should not be allowed.") except IndexError: pass # as it should be try: va.value = (10., 10.) self.fail( "Assigning a 2-tuple to a 3-tuple should not be allowed.") except TypeError: pass # as it should be # Test creating a VA with value out of range with self.assertRaises(IndexError): va = model.TupleContinuous((0.1, 10), ((-1.3, 12), (100., 150.)), cls=(int, long, float))
def __init__(self, name, detector, sed, emitter, opm=None): """ name (string): user-friendly name of this stream detector (Detector): the monochromator sed (Detector): the se-detector emitter (Emitter): the emitter (eg: ebeam scanner) spectrograph (Actuator): the spectrograph """ self.name = model.StringVA(name) # Hardware Components, detector is the correlator, sed is the secondary electron image and the emitter is the electron beam self._detector = detector self._sed = sed self._emitter = emitter self._opm = opm self.is_active = model.BooleanVA(False) #dwell time and exposure time are the same thing in this case self.dwellTime = model.FloatContinuous(1, range=self._emitter.dwellTime.range, unit="s") # pixelDuration of correlator, this can be shortened once implemented as choices. self.pixelDuration = model.FloatEnumerated(512e-12, choices={4e-12, 8e-12, 16e-12, 32e-12, 64e-12, 128e-12, 256e-12, 512e-12}, unit="s", ) #Sync Offset time correlator self.syncOffset = self._detector.syncOffset #Sync Divider time correlator self.syncDiv = self._detector.syncDiv # Distance between the center of each pixel self.stepsize = model.FloatContinuous(1e-6, (1e-9, 1e-4), unit="m") # Region of acquisition. ROI form is LEFT Top RIGHT Bottom, relative to full field size self.roi = model.TupleContinuous((0, 0, 1, 1), range=((0, 0, 0, 0), (1, 1, 1, 1)), cls=(int, long, float)) # Cropvalue that can be used to crop the data for better visualization in odemis self.cropvalue = model.IntContinuous(1024, (1, 65536), unit="px") # For drift correction self.dcRegion = model.TupleContinuous(UNDEFINED_ROI, range=((0, 0, 0, 0), (1, 1, 1, 1)), cls=(int, long, float)) self.dcDwellTime = model.FloatContinuous(emitter.dwellTime.range[0], range=emitter.dwellTime.range, unit="s") #number of drift corrections per scanning pixel self.nDC = model.IntContinuous(1, (1, 20)) # For acquisition self.tc_data = None self.tc_data_received = threading.Event() self.sem_data = [] self.sem_data_received = threading.Event() self._hw_settings = None
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, name, coordinates, roc, asm, multibeam, descanner, detector): """ :param name: (str) Name of the region of acquisition (ROA). It is the name of the megafield (id) as stored on the external storage. :param coordinates: (float, float, float, float) left, top, right, bottom, Bounding box coordinates of the ROA in [m]. The coordinates are in the sample carrier coordinate system, which corresponds to the component with role='stage'. :param roc: (FastEMROC) Corresponding region of calibration (ROC). :param asm: (technolution.AcquisitionServer) The acquisition server module component. :param multibeam: (technolution.EBeamScanner) The multibeam scanner component of the acquisition server module. :param descanner: (technolution.MirrorDescanner) The mirror descanner component of the acquisition server module. :param detector: (technolution.MPPC) The detector object to be used for collecting the image data. """ self.name = model.StringVA(name) self.coordinates = model.TupleContinuous(coordinates, range=((-1, -1, -1, -1), (1, 1, 1, 1)), cls=(int, float), unit='m') self.roc = model.VigilantAttribute(roc) self._asm = asm self._multibeam = multibeam self._descanner = descanner self._detector = detector # List of tuples(int, int) containing the position indices of each field to be acquired. # Automatically updated when the coordinates change. self.field_indices = [] self.coordinates.subscribe(self.on_coordinates, init=True)
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 __init__(self, name, detector, dataflow, emitter, forcemd=None, **kwargs): """ forcemd (None or dict of MD_* -> value): force the metadata of the .image DataArray to be overridden by this metadata. """ super(LiveStream, self).__init__(name, detector, dataflow, emitter, **kwargs) self._forcemd = forcemd 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._ht_needs_recompute = threading.Event() self._hthread = threading.Thread(target=self._histogram_thread, args=(weakref.ref(self),), name="Histogram computation") self._hthread.daemon = True self._hthread.start() self._prev_dur = None self._prep_future = model.InstantaneousFuture()
def __init__(self, scanner, detector): """ :param scanner: (Emitter) A component with a .dwellTime, .translation, .scale. :param detector: (Detector) To acquire the signal. """ super(AnchorDriftCorrector, self).__init__() self._scanner = scanner self._detector = detector self._dc_estimator = None self._period_acq = None # number of acq left until next drift correction is performed # roi: the anchor region, it must be set to something different from # UNDEFINED_ROI to run. # dwellTime: dwell time used when acquiring anchor region # period is the (approximate) time between two acquisition of the # anchor (and drift compensation). The exact period is determined so # that it fits with the region of acquisition. # Note: the scale used for the acquisition of the anchor region is # selected by the AnchoredEstimator, to be as small as possible while # still not scanning too many pixels. self.roi = model.TupleContinuous(UNDEFINED_ROI, range=((0, 0, 0, 0), (1, 1, 1, 1)), cls=(int, long, float), setter=self._setROI) self.dwellTime = model.FloatContinuous(scanner.dwellTime.range[0], range=scanner.dwellTime.range, unit="s") # in seconds, default to "fairly frequent" to work hopefully in most cases self.period = model.FloatContinuous(10, range=(0.1, 1e6), unit="s")
def __init__(self, name, detector, emitter, spectrograph, opm=None): """ name (string): user-friendly name of this stream detector (Detector): the monochromator emitter (Emitter): the emitter (eg: ebeam scanner) spectrograph (Actuator): the spectrograph """ self.name = model.StringVA(name) # Hardware Components self._detector = detector self._emitter = emitter self._sgr = spectrograph self._opm = opm self.is_active = model.BooleanVA(False) wlr = spectrograph.axes["wavelength"].range self.startWavelength = model.FloatContinuous(400e-9, wlr, unit="m") self.endWavelength = model.FloatContinuous(500e-9, wlr, unit="m") self.numberOfPixels = model.IntContinuous(51, (2, 10001), unit="px") # TODO: could be a local attribute? self.dwellTime = model.FloatContinuous( 1e-3, range=self._emitter.dwellTime.range, unit="s") self.emtTranslation = model.TupleContinuous( (0, 0), range=self._emitter.translation.range, cls=(int, long, float), unit="px") # For acquisition self._pt_acq = threading.Event() self._data = [] self._md = {}
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, 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, coordinates): """ :param name: (str) name of the ROC :param coordinates: (float, float, float, float) l, t, r, b coordinates in m """ self.name = model.StringVA(name) self.coordinates = model.TupleContinuous(coordinates, range=((-1, -1, -1, -1), (1, 1, 1, 1)), cls=(int, float), unit='m') self.parameters = None # calibration object with all relevant parameters
def __init__(self, name, coordinates, roc): """ :param name: (str) name of the ROA :param coordinates: (float, float, float, float) l, t, r, b coordinates in m :param roc: (FastEMROC) corresponding region of calibration """ self.name = model.StringVA(name) self.coordinates = model.TupleContinuous(coordinates, range=((-1, -1, -1, -1), (1, 1, 1, 1)), cls=(int, float), unit='m') self.roc = model.VigilantAttribute(roc)
def __init__(self, name, coordinates, roc_2, roc_3, asm, multibeam, descanner, detector, overlap=0, pre_calibrate=False): """ :param name: (str) Name of the region of acquisition (ROA). It is the name of the megafield (id) as stored on the external storage. :param coordinates: (float, float, float, float) xmin, ymin, xmax, ymax Bounding box coordinates of the ROA in [m]. The coordinates are in the sample carrier coordinate system, which corresponds to the component with role='stage'. :param roc_2: (FastEMROC) Corresponding region of calibration (ROC). Used for dark offset and digital gain calibration. :param roc_3: (FastEMROC) Corresponding region of calibration (ROC). Used for the final scanner rotation, final scanning amplitude and cell translation (cell stitching) calibration (field corrections). :param asm: (technolution.AcquisitionServer) The acquisition server module component. :param multibeam: (technolution.EBeamScanner) The multibeam scanner component of the acquisition server module. :param descanner: (technolution.MirrorDescanner) The mirror descanner component of the acquisition server module. :param detector: (technolution.MPPC) The detector object to be used for collecting the image data. :param overlap: (float), optional The amount of overlap required between single fields. An overlap of 0.2 means that two neighboring fields overlap by 20%. By default, the overlap is 0, this means there is no overlap and one field is exactly next to the neighboring field. :param pre_calibrate: (bool) If True run pre-calibrations before each ROA acquisition. """ self.name = model.StringVA(name) self.coordinates = model.TupleContinuous(coordinates, range=((-1, -1, -1, -1), (1, 1, 1, 1)), cls=(int, float), unit='m') self.roc_2 = model.VigilantAttribute(roc_2) self.roc_3 = model.VigilantAttribute(roc_3) self._asm = asm self._multibeam = multibeam self._descanner = descanner self._detector = detector # List of tuples(int, int) containing the position indices of each field to be acquired. # Automatically updated when the coordinates change. self.field_indices = [] self.overlap = overlap self.pre_calibrate = pre_calibrate self.coordinates.subscribe(self.on_coordinates, init=True)
def __init__(self, name, detector, dataflow, emitter): Stream.__init__(self, name, detector, dataflow, emitter) # TODO: Anti-aliasing/Pixel fuzzing # .fuzzing: boolean # Might be better to automatically activate it for Spectrum, and disable # it for AR (without asking the user) try: self._prevDwellTime = emitter.dwellTime.value emitter.dwellTime.subscribe(self.onDwellTime) except AttributeError: # if emitter has no dwell time -> no problem pass # Actually use the ROI self.roi.subscribe(self._onROI) # Spot mode: when set (and stream is active), it will drive the e-beam # do only the center of the scanning area. Image is not updated. # TODO: is this the right interface? Shall we just have a different # stream type? self.spot = model.BooleanVA(False) # used to reset the previous settings after spot mode self._no_spot_settings = (None, None, None ) # dwell time, resolution, translation self.spot.subscribe(self._onSpot) # drift correction VAs: # dcRegion defines the anchor region, drift correction will be disabled # if it is set to UNDEFINED_ROI # dcDwellTime: dwell time used when acquiring anchor region # dcPeriod is the (approximate) time between two acquisition of the # anchor (and drift compensation). The exact period is determined so # that it fits with the region of acquisition. # Note: the scale used for the acquisition of the anchor region is the # same as the scale of the SEM. We could add a dcScale if it's needed. self.dcRegion = model.TupleContinuous(UNDEFINED_ROI, range=((0, 0, 0, 0), (1, 1, 1, 1)), cls=(int, long, float), setter=self._setDCRegion) self.dcDwellTime = model.FloatContinuous(emitter.dwellTime.range[0], range=emitter.dwellTime.range, unit="s") self.dcPeriod = model.FloatContinuous( 10, # s, default to "fairly frequent" to work hopefully in most cases range=[0.1, 1e6], unit="s")
def __init__(self, name, coordinates): """ :param name: (str) Name of the region of calibration (ROC). It is the name of the megafield (id) as stored on the external storage. :param coordinates: (float, float, float, float) left, top, right, bottom, Bounding box coordinates of the ROC in [m]. The coordinates are in the sample carrier coordinate system, which corresponds to the component with role='stage'. """ self.name = model.StringVA(name) self.coordinates = model.TupleContinuous(coordinates, range=((-1, -1, -1, -1), (1, 1, 1, 1)), cls=(int, float), unit='m') self.parameters = None # calibration object with all relevant parameters
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, stream): ''' stream (Stream): the Stream to project ''' super(RGBSpatialProjection, self).__init__(stream) self.should_update = model.BooleanVA(False) self.name = stream.name self.image = model.VigilantAttribute(None) # Don't call at init, so don't set metadata if default value self.stream.tint.subscribe(self._onTint) self.stream.intensityRange.subscribe(self._onIntensityRange) self.stream.auto_bc.subscribe(self._onAutoBC) self.stream.auto_bc_outliers.subscribe(self._onOutliers) if hasattr(stream, '_das'): raw = stream._das md = raw.metadata # get the pixel size of the full image ps = md[model.MD_PIXEL_SIZE] max_mpp = ps[0] * (2**raw.maxzoom) # sets the mpp as the X axis of the pixel size of the full image mpp_rng = (ps[0], max_mpp) self.mpp = model.FloatContinuous(max_mpp, mpp_rng, setter=self._set_mpp) full_rect = img._getBoundingBox(raw) l, t, r, b = full_rect rect_range = ((l, b, l, b), (r, t, r, t)) self.rect = model.TupleContinuous(full_rect, rect_range) self.mpp.subscribe(self._onMpp) self.rect.subscribe(self._onRect) # initialize the projected tiles cache self._projectedTilesCache = {} # initialize the raw tiles cache self._rawTilesCache = {} # When True, the projected tiles cache should be invalidated self._projectedTilesInvalid = True self._shouldUpdateImage()
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, port=None, serialnum=None, dependencies=None, **kwargs): """ :param port (str): (e.g. "/dev/ttyUSB0") or pattern for port ("/dev/ttyUSB*"), "/dev/fake" will start the simulator :param serialnum (str): serial number of RS485 adapter The connection can be specified by either port or serialnum, it's not needed to provide both. :param dependencies (dict str -> scanner): scanner component -> name of the xt multibeam scanner component. If None, no calibration data, which is specific for the multibeam system, will be retrieved from the scanner and added to the beamshift metadata. """ # .hwVersion, .swVersion not available model.HwComponent.__init__(self, name, role, **kwargs) # Find port by RS485 adapter serial number self._portpattern = port self._serialnum = serialnum self._port = self._findDevice(port, serialnum) self._serial = self._openSerialPort(self._port) # Shift VA # Range depends on metadata and will be checked in ._write_registers # The value is not correct until the metadata is set. self.shift = model.TupleContinuous((0, 0), range=((-1, -1), (1, 1)), cls=(int, float), unit="m", setter=self._setShift) if dependencies and "scanner" in dependencies.keys(): self.updateMetadata({ model.MD_CALIB: dependencies["scanner"].beamShiftTransformationMatrix.value })
def __init__(self, name, detector, dataflow, emitter, **kwargs): super(SEMStream, self).__init__(name, detector, dataflow, emitter, **kwargs) # To restart directly acquisition if settings change try: self._getEmitterVA("dwellTime").subscribe(self._onDwellTime) except AttributeError: # if emitter has no dwell time -> no problem pass try: # Resolution picks up also scale and ROI change self._getEmitterVA("resolution").subscribe(self._onResolution) except AttributeError: pass # Actually use the ROI self.roi.subscribe(self._onROI) # drift correction VAs: # Not currently supported by this standard stream, but some synchronised # streams do. # dcRegion defines the anchor region, drift correction will be disabled # if it is set to UNDEFINED_ROI # dcDwellTime: dwell time used when acquiring anchor region # dcPeriod is the (approximate) time between two acquisition of the # anchor (and drift compensation). The exact period is determined so # that it fits with the region of acquisition. # Note: the scale used for the acquisition of the anchor region is the # same as the scale of the SEM. We could add a dcScale if it's needed. self.dcRegion = model.TupleContinuous(UNDEFINED_ROI, range=((0, 0, 0, 0), (1, 1, 1, 1)), cls=(int, long, float), setter=self._setDCRegion) self.dcDwellTime = model.FloatContinuous(emitter.dwellTime.range[0], range=emitter.dwellTime.range, unit="s") # in seconds, default to "fairly frequent" to work hopefully in most cases self.dcPeriod = model.FloatContinuous(10, range=(0.1, 1e6), unit="s")
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 __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, 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, 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, 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, detector, sed, emitter, spectrograph, lens_switch, bigslit, opm, wl_inverted=False): """ name (string): user-friendly name of this stream detector (Detector): the 2D CCD which get wavelength on the X axis and angles on the Y axis sed (Detector): the se-detector emitter (Emitter): the emitter (eg: ebeam scanner) spectrograph (Actuator): the spectrograph wl_inverted (bool): if True, will swap the wavelength axis of the CCD, in order to support hardware where the highest wavelengths are at the smallest indices. (The MD_WL_LIST is *not* inverted) """ self.name = model.StringVA(name) # Hardware Components self._detector = detector self._sed = sed self._emitter = emitter self._sgr = spectrograph self._opm = opm self._lsw = lens_switch self._bigslit = bigslit self._wl_inverted = wl_inverted wlr = spectrograph.axes["wavelength"].range slitw = spectrograph.axes["slit-in"].range self.centerWavelength = model.FloatContinuous(500e-9, wlr, unit="m") self.slitWidth = model.FloatContinuous(100e-6, slitw, unit="m") # dwell time and exposure time are the same thing in this case self.dwellTime = model.FloatContinuous( 1, range=detector.exposureTime.range, unit="s") self.emtTranslation = model.TupleContinuous( (0, 0), range=self._emitter.translation.range, cls=(int, long, float), unit="px") # Distance between the center of each pixel self.stepsize = model.FloatContinuous(1e-6, (1e-9, 1e-4), unit="m") # Region of acquisition. ROI form is LEFT Top RIGHT Bottom, relative to full field size self.roi = model.TupleContinuous((0, 0, 1, 1), range=((0, 0, 0, 0), (1, 1, 1, 1)), cls=(int, long, float)) # For drift correction self.dcRegion = model.TupleContinuous(UNDEFINED_ROI, range=((0, 0, 0, 0), (1, 1, 1, 1)), cls=(int, long, float)) self.dcDwellTime = model.FloatContinuous(emitter.dwellTime.range[0], range=emitter.dwellTime.range, unit="s") #self.binning = model.VAEnumerated((1,1), choices=set([(1,1), (2,2), (2,3)])) # separate binning values because it can useful for experiment self.binninghorz = model.VAEnumerated(1, choices={1, 2, 4, 8, 16}) self.binningvert = model.VAEnumerated(1, choices={1, 2, 4, 8, 16}) self.nDC = model.IntContinuous(1, (1, 20)) # For acquisition self.ARspectral_data = None self.ARspectral_data_received = threading.Event() self.sem_data = [] self.sem_data_received = threading.Event() self._hw_settings = None
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, parent, hfw_nomag, **kwargs): model.Emitter.__init__(self, name, role, parent=parent, **kwargs) self._hfw_nomag = hfw_nomag dwell_time_info = self.parent.dwell_time_info() self.dwellTime = model.FloatContinuous(self.parent.get_dwell_time(), dwell_time_info["range"], unit=dwell_time_info["unit"], setter=self._setDwellTime) voltage_info = self.parent.ht_voltage_info() self.accelVoltage = model.FloatContinuous(self.parent.get_ht_voltage(), voltage_info["range"], unit=voltage_info["unit"], setter=self._setVoltage) self.blanker = model.BooleanVA(self.parent.beam_is_blanked(), setter=self._setBlanker) spotsize_info = self.parent.spotsize_info() self.spotSize = model.FloatContinuous(self.parent.get_ebeam_spotsize(), spotsize_info["range"], unit=spotsize_info["unit"], setter=self._setSpotSize) beam_shift_info = self.parent.beam_shift_info() range_x = beam_shift_info["range"]["x"] range_y = beam_shift_info["range"]["y"] self.beamShift = model.TupleContinuous(self.parent.get_beam_shift(), ((range_x[0], range_y[0]), (range_x[1], range_y[1])), cls=(int, float), unit=beam_shift_info["unit"], setter=self._setBeamShift) rotation_info = self.parent.rotation_info() self.rotation = model.FloatContinuous(self.parent.get_rotation(), rotation_info["range"], unit=rotation_info["unit"], setter=self._setRotation) scanning_size_info = self.parent.scanning_size_info() fov = self.parent.get_scanning_size()[0] self.horizontalFoV = model.FloatContinuous( fov, unit=scanning_size_info["unit"], range=scanning_size_info["range"]["x"], setter=self._setHorizontalFoV) mag = self._hfw_nomag / fov mag_range_max = self._hfw_nomag / scanning_size_info["range"]["x"][0] mag_range_min = self._hfw_nomag / scanning_size_info["range"]["x"][1] self.magnification = model.FloatContinuous(mag, unit="", range=(mag_range_min, mag_range_max), readonly=True) # 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, 1e3), unit="m", readonly=True) self._updateDepthOfField() # Refresh regularly the values, from the hardware, starting from now self._updateSettings() self._va_poll = util.RepeatingTimer(5, self._updateSettings, "Settings polling") self._va_poll.start()
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, 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_*