def __init__(self, microscope, main_app): super(RGBCLIntensity, self).__init__(microscope, main_app) # Can only be used on a SPARC with a CL-intensity detector if not microscope: return try: self.ebeam = model.getComponent(role="e-beam") self.cldetector = model.getComponent(role="cl-detector") self.filterwheel = model.getComponent(role="cl-filter") self.sed = model.getComponent(role="se-detector") # We could also check the filter wheel has at least 3 filters, but # let's not be too picky, if the user has installed the plugin, he # probably wants to use it anyway. except LookupError: logging.info("Hardware not found, cannot use the RGB CL plugin") return # The SEM survey and CLi stream (will be updated when showing the window) self._survey_s = None self._cl_int_s = None self._acqui_tab = main_app.main_data.getTabByName( "sparc_acqui").tab_data_model # The settings to be displayed in the dialog # TODO: pick better default filters than first 3 filters # => based on the wavelengths fitting best RGB, or the names (eg, "Blue"), # and avoid "pass-through". fbchoices = self.filterwheel.axes["band"].choices if isinstance(fbchoices, dict): fbvalues = sorted(fbchoices.keys()) else: fbvalues = fbchoices # FloatEnumerated because filter positions can be in rad (ie, not int positions) self.filter1 = model.FloatEnumerated(fbvalues[0], choices=fbchoices) self.filter2 = model.FloatEnumerated(fbvalues[min( 1, len(fbvalues) - 1)], choices=fbchoices) self.filter3 = model.FloatEnumerated(fbvalues[min( 2, len(fbvalues) - 1)], choices=fbchoices) self._filters = [self.filter1, self.filter2, self.filter3] self._colours = [(0, 0, 255), (0, 255, 0), (255, 0, 0)] # B, G, R self.filename = model.StringVA("a.tiff") self.expectedDuration = model.VigilantAttribute(1, unit="s", readonly=True) self.addMenu("Acquisition/RGB CL intensity...", self.start)
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, role, parent, daemon=None, **kwargs): super(StreakUnit, self).__init__(name, role, parent=parent, daemon=daemon, **kwargs) # init HwComponent self.parent = parent self._metadata[model.MD_HW_VERSION] = "Simulated streak unit C10627" # VAs self.streakMode = model.BooleanVA( False, setter=self._setStreakMode) # default False see set params above gain = 0 range_gain = (0, 63) self.MCPGain = model.IntContinuous(gain, range_gain, setter=self._setMCPGain) timeRange = 0.000000001 choices = { 0.000000001, 0.000000002, 0.000000005, 0.00000001, 0.00000002, 0.00000005, 0.0000001, 0.0000002, 0.0000005, 0.000001, 0.000002, 0.000005, 0.00001, 0.00002, 0.00005, 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01 } self.timeRange = model.FloatEnumerated(timeRange, choices, setter=self._setTimeRange, unit="s") self._metadata[model.MD_STREAK_TIMERANGE] = self.timeRange.value self._metadata[model.MD_STREAK_MCPGAIN] = self.MCPGain.value self._metadata[model.MD_STREAK_MODE] = self.streakMode.value
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, 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, 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, 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, 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, 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'})