def test_configure_scanner_overview(self): """Check that for the overview mode, the correct HW settings are set on the respective scanner VAs.""" fastem_conf.configure_scanner(self.scanner, OVERVIEW_MODE) self.assertFalse(self.scanner.multiBeamMode.value) self.assertFalse(self.scanner.external.value) self.assertFalse(self.scanner.blanker.value) self.assertFalse(self.scanner.immersion.value) self.assertGreater(self.scanner.horizontalFoV.value, 1.e-3) # should be big FoV for overview self.assertEqual(self.scanner.rotation.value, math.radians(5)) scanner_md = self.scanner.getMetadata() # check that the MD_POS_COR is correctly set for overview imaging. self.assertListEqual(scanner_md[model.MD_FIELD_FREE_POS_SHIFT], scanner_md[model.MD_POS_COR]) # check rotation set is also stored in MD as rotation correction self.assertEqual(scanner_md[model.MD_ROTATION_COR], math.radians(5)) # acquire an image and check the MD is correct: ROTATION - ROTATION_COR == 0 image = self.sed.data.get() self.assertAlmostEqual( image.metadata[model.MD_ROTATION] - image.metadata[model.MD_ROTATION_COR], 0) # merge the MD -> automatically calculates rotation - rotation_cor -> puts result on rotation in MD img.mergeMetadata(image.metadata) self.assertAlmostEqual(image.metadata[model.MD_ROTATION], 0)
def test_configure_scanner_megafield(self): """Check that for megafield mode, the correct HW settings are set on the respective scanner VAs.""" fastem_conf.configure_scanner(self.scanner, MEGAFIELD_MODE) self.assertTrue(self.scanner.multiBeamMode.value) self.assertTrue(self.scanner.external.value) self.assertFalse(self.scanner.blanker.value) self.assertTrue(self.scanner.immersion.value) self.assertLess( self.scanner.horizontalFoV.value, 0.1e-3) # should be smaller for megafield than for overview self.assertEqual(self.scanner.rotation.value, math.radians(10)) # Changes in the rotation on the scanner (e-beam) should be reflected in the MD on the multibeam. # check that rotation correction is 0 or not existing for megafield imaging using the ASM/SAM self.assertEqual( self.multibeam.getMetadata().get(model.MD_ROTATION_COR, 0), 0) # check that rotation is the same as was specified for the scanner (e-beam) for megafield imaging self.assertEqual(self.multibeam.getMetadata()[model.MD_ROTATION], math.radians(10)) # check that the MD_POS_COR is set to [0, 0] for live stream imaging. self.assertListEqual([0, 0], self.scanner.getMetadata()[model.MD_POS_COR])
def test_configure_scanner_live(self): """Check that for the live mode, the correct HW settings are set on the respective scanner VAs.""" fastem_conf.configure_scanner(self.scanner, LIVESTREAM_MODE) self.assertFalse(self.scanner.multiBeamMode.value) self.assertFalse(self.scanner.external.value) self.assertFalse(self.scanner.blanker.value) self.assertTrue(self.scanner.immersion.value) self.assertEqual(self.scanner.rotation.value, math.radians(5)) scanner_md = self.scanner.getMetadata() # check that the MD_POS_COR is set to [0, 0] for live stream imaging. self.assertListEqual([0, 0], scanner_md[model.MD_POS_COR])
def _run_overview_acquisition(f, stream, stage, area, live_stream): """ Runs the acquisition of one overview image (typically one scintillator). :param f: (ProgressiveFuture) Acquisition future object. :param stream: (SEMstream) The stream used for the acquisition. :param stage: (actuator.MultiplexActuator) The stage in the sample carrier coordinate system. The x and y axes are aligned with the x and y axes of the ebeam scanner. :param area: (float, float, float, float) xmin, ymin, xmax, ymax coordinates of the overview region. :param live_stream: (StaticStream or None): StaticStream to be updated with each tile acquired, to build up live the whole acquisition. NOT SUPPORTED YET. :returns: (DataArray) The complete overview image. """ fastem_conf.configure_scanner(stream.emitter, fastem_conf.OVERVIEW_MODE) # The stage movement precision is quite good (just a few pixels). The stage's # position reading is much better, and we can assume it's below a pixel. # So as long as we are sure there is some overlap, the tiles will be positioned # correctly and without gap. overlap = STAGE_PRECISION / stream.emitter.horizontalFoV.value logging.debug("Overlap is %s%%", overlap * 100) # normally < 1% def _pass_future_progress(sub_f, start, end): f.set_progress(start, end) # Note, for debugging, it's possible to keep the intermediary tiles with log_path="./tile.ome.tiff" sf = stitching.acquireTiledArea([stream], stage, area, overlap, registrar=REGISTER_IDENTITY, focusing_method=FocusingMethod.NONE) # Connect the progress of the underlying future to the main future # FIXME removed to provide better progress update in GUI # When _tiledacq.py has proper time update implemented, add this line here again. # sf.add_update_callback(_pass_future_progress) das = sf.result() if len(das) != 1: logging.warning("Expected 1 DataArray, but got %d: %r", len(das), das) # Switch immersion mode back on, so we can focus the SEM from the TFS GUI. stream.emitter.immersion.value = True # FIXME auto blanking not working properly, so force beam blanking after image acquisition for now. stream.emitter.blanker.value = True return das[0]
def _run_overview_acquisition(f, stream, stage, area, live_stream): """ :returns: (DataArray) """ fastem_conf.configure_scanner(stream.emitter, fastem_conf.OVERVIEW_MODE) # The stage movement precision is quite good (just a few pixels). The stage's # position reading is much better, and we can assume it's below a pixel. # So as long as we are sure there is some overlap, the tiles will be positioned # correctly and without gap. overlap = STAGE_PRECISION / stream.emitter.horizontalFoV.value logging.debug("Overlap is %s%%", overlap * 100) # normally < 1% def _pass_future_progress(sub_f, start, end): f.set_progress(start, end) # Note, for debugging, it's possible to keep the intermediary tiles with log_path="./tile.ome.tiff" sf = stitching.acquireTiledArea([stream], stage, area, overlap, registrar=REGISTER_IDENTITY, focusing_method=FocusingMethod.NONE) # Connect the progress of the underlying future to the main future sf.add_update_callback(_pass_future_progress) das = sf.result() if len(das) != 1: logging.warning("Expected 1 DataArray, but got %d: %r", len(das), das) # Switch immersion mode back on, so we can focus the SEM from the TFS GUI. stream.emitter.immersion.value = True # FIXME auto blanking not working properly, so force beam blanking after image acquisition for now. stream.emitter.blanker.value = True return das[0]
def run(self): """ Runs the acquisition of one ROA (megafield). :returns: megafield: (list of DataArrays) A list of the raw image data. Each data array (entire field, thumbnail, or zero array) represents one single field image within the roa (megafield). exception: (Exception or None) Exception raised during the acquisition. If some single field image data has already been acquired, exceptions are not raised, but returned. :raise: Exception: If it failed before any single field images were acquired or if acquisition was cancelled. """ # Get the estimated time for the roa. total_roa_time = estimate_acquisition_time(self._roa, self._pre_calibrations) # No need to set the start time of the future: it's automatically done when setting its state to running. self._future.set_progress(end=time.time() + total_roa_time) # provide end time to future logging.info( "Starting acquisition of ROA %s, with expected duration of %f s and %s by %s fields.", self._roa.name, total_roa_time, self._roa.field_indices[-1][0] + 1, self._roa.field_indices[-1][1] + 1) # Update the position of the first tile. self._pos_first_tile = self.get_pos_first_tile() if self._pre_calibrations: # The pre-calibrations should run on a position that lies a full field # outside the ROA, therefore temporarily set the overlap to zero. overlap_init = self._roa.overlap self._roa.overlap = 0 # Move the stage to the tile with index (-1, -1), to ensure the autofocus and image translation pre-align # are done to the top left of the first field, outside the region of acquisition to limit beam damage. self.field_idx = (-1, -1) self.pre_calibrate(self._pre_calibrations) self._roa.overlap = overlap_init # set back the overlap to the initial value # Move the stage to the first tile, to ensure the correct position is # stored in the megafield metadata yaml file. self.field_idx = (0, 0) self.move_stage_to_next_tile() # set the sub-directories (<acquisition date>/<project name>) and megafield id self._detector.filename.value = os.path.join(self._path, self._roa.name.value) exception = None dataflow = self._detector.data try: logging.debug("Configure hardware for acquisition.") # configure the HW settings fastem_conf.configure_scanner(self._scanner, fastem_conf.MEGAFIELD_MODE) fastem_conf.configure_detector(self._detector, self._rocs) dataflow.subscribe(self.image_received) # Acquire the single field images. self.acquire_roa(dataflow) except CancelledError: # raised in acquire_roa() logging.debug("Acquisition was cancelled.") raise except Exception as ex: # Check if any field images have already been acquired; if not => just raise the exception. if len(self._fields_remaining) == len(self._roa.field_indices): raise # If image data was already acquired, just log a warning. logging.warning( "Exception during roa acquisition (after some data has already been acquired).", exc_info=True) exception = ex # let the caller handle the exception finally: # Remove references to the megafield once the acquisition is finished/cancelled. self._fields_remaining.clear() # Blank the beam after the acquisition is done. self._scanner.blanker.value = True # Finish the megafield also if an exception was raised, in order to enable a new acquisition. logging.debug("Finish ROA acquisition.") dataflow.unsubscribe(self.image_received) return self.megafield, exception
def run(self): """ Runs the acquisition of one ROA (megafield). :returns: megafield: (list of DataArrays) A list of the raw image data. Each data array (entire field, thumbnail, or zero array) represents one single field image within the roa (megafield). exception: (Exception or None) Exception raised during the acquisition. If some single field image data has already been acquired, exceptions are not raised, but returned. :raise: Exception: If it failed before any single field images were acquired or if acquisition was cancelled. """ # set the sub-directories (<acquisition date>/<project name>) and megafield id self._detector.filename.value = os.path.join(self._path, self._roa.name.value) exception = None # Get the estimated time for the roa. total_roa_time = self._roa.estimate_acquisition_time() # No need to set the start time of the future: it's automatically done when setting its state to running. self._future.set_progress(end=time.time() + total_roa_time) # provide end time to future logging.info( "Starting acquisition of mega field, with expected duration of %f s", total_roa_time) dataflow = self._detector.data try: logging.debug("Starting megafield acquisition of %s by %s fields.", self._roa.field_indices[-1][0] + 1, self._roa.field_indices[-1][1] + 1) # configure the HW settings fastem_conf.configure_scanner(self._scanner, fastem_conf.MEGAFIELD_MODE) dataflow.subscribe(self.image_received) # Acquire the single field images. self.acquire_roa(dataflow) except CancelledError: # raised in acquire_roa() logging.debug("Acquisition was cancelled.") raise except Exception as ex: # Check if any field images have already been acquired; if not => just raise the exception. if len(self._fields_remaining) == len(self._roa.field_indices): raise # If image data was already acquired, just log a warning. logging.warning( "Exception during roa acquisition (after some data has already been acquired).", exc_info=True) exception = ex # let the caller handle the exception finally: # Remove references to the megafield once the acquisition is finished/cancelled. self._fields_remaining.clear() # Blank the beam after the acquisition is done. self._scanner.blanker.value = True # Finish the megafield also if an exception was raised, in order to enable a new acquisition. logging.debug("Finish megafield acquisition.") dataflow.unsubscribe(self.image_received) return self.megafield, exception
def pre_calibrate(self): """ Run optical multiprobe autofocus and image translation pre-alignment before the ROA acquisition. The image translation pre-alignment adjusts the descanner.scanOffset VA such that the image of the multiprobe is roughly centered on the mppc detector. This function reads in the ASM configuration and makes sure all values, except the descanner offset, are set back after the calibrations are run. NOTE: Canceling is currently not supported. """ if not fastem_calibrations: raise ModuleNotFoundError( "Need fastem_calibrations repository to run pre-calibrations.") asm_config = None try: logging.debug("Read initial Hw settings.") asm_config = configure_hw.get_config_asm(self._multibeam, self._descanner, self._detector) fastem_conf.configure_scanner(self._scanner, fastem_conf.MEGAFIELD_MODE) # Set the beamshift to zero before adjusting the descanner offset, this ensures that the maximum beamshift # range can be utilized when the pattern is centered on the mppc detector. self._beamshift.shift.value = (0, 0) # Autofocus multiprobe ensures the beams are focused at the start of the ROA acquisition. logging.debug("Running optical autofocus before ROA acquisition.") autofocus_multiprobe.run_autofocus(self._scanner, self._multibeam, self._descanner, self._detector, self._detector.data, self._ccd, self._stage) configure_hw.configure_asm(self._multibeam, self._descanner, self._detector, self._detector.data, asm_config, upload=False) # Image translation pre-alignment ensures that the image of the multiprobe is roughly centered on the # mppc detector. logging.debug( "Running image translation pre alignment before ROA acquisition." ) descanner_offset = image_translation_pre_align.run_image_translation_pre_align( self._scanner, self._multibeam, self._descanner, self._detector, self._detector.data, self._ccd) # Set the descanner offset to the value calibrated with the image translation pre-alignment. asm_config["descanner"]["scanOffset"] = descanner_offset logging.debug(f"Descanner offset set to: {descanner_offset}") # Blank the beam to reduce beam damage on the sample. self._scanner.blanker.value = True except Exception as ex: logging.warning( "Exception during pre-calibration of the ROA acquisition.", exc_info=True) raise finally: # Blank the beam after the acquisition is done. self._scanner.blanker.value = True if asm_config is None: logging.warning( "Failed to retrieve asm configuration, configure_asm cannot be executed." ) else: # put system into state ready for next task # upload=False: it is enough to set calibrated values on HW during following acquisition configure_hw.configure_asm(self._multibeam, self._descanner, self._detector, self._detector.data, asm_config, upload=False) logging.debug("Finish pre-calibration.")