def exception(self, timeout=None): """Return the exception raised by the call that the future represents. Args: timeout: The number of seconds to wait for the exception if the future isn't done. If None, then there is no limit on the wait time. Returns: The exception raised by the call that the future represents or None if the call completed without raising. Raises: CancelledError: If the future was cancelled. TimeoutError: If the future didn't finish executing before the given timeout. """ with self._condition: if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]: raise CancelledError() elif self._state == FINISHED: return self._exception self._condition.wait(timeout) if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]: raise CancelledError() elif self._state == FINISHED: return self._exception else: raise TimeoutError()
def result(self, timeout=None): """Return the result of the call that the future represents. Args: timeout: The number of seconds to wait for the result if the future isn't done. If None, then there is no limit on the wait time. Returns: The result of the call that the future represents. Raises: CancelledError: If the future was cancelled. TimeoutError: If the future didn't finish executing before the given timeout. Exception: If the call raised then that exception will be raised. """ with self._condition: if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]: raise CancelledError() elif self._state == FINISHED: return self.__get_result() self._condition.wait(timeout) if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]: raise CancelledError() elif self._state == FINISHED: return self.__get_result() else: raise TimeoutError()
async def _run_with_tg(self, *, evt: anyio.abc.Event=None): try: await super()._run_with_tg(evt=evt) except anyio.get_cancelled_exc_class(): if self._done.is_set(): await self._handle_prev(_ResultEvent(self._result)) else: await self._handle_prev(_ErrorEvent(CancelledError())) raise except Exception as exc: await self._handle_prev(_ErrorEvent(exc)) except BaseException: await self._handle_prev(_ErrorEvent(CancelledError())) raise else: await self._handle_prev(_ResultEvent(self._result))
def _acquireStreamTile(self, i, ix, iy, stream): """ Calls acquire function and blocks until the data is returned. :return DataArray: Acquired da for the current tile stream """ # Update the progress bar self._future.set_progress( end=self.estimateTime((self._nx * self._ny) - i) + time.time()) # Acquire data array for passed stream self._future.running_subf = acqmng.acquire([stream], self._settings_obs) das, e = self._future.running_subf.result( ) # blocks until all the acquisitions are finished if e: logging.warning( f"Acquisition for tile {ix}x{iy}, stream {stream.name} partially failed: {e}" ) if self._future._task_state == CANCELLED: raise CancelledError() try: return das[0] # return first da except IndexError: raise IndexError( f"Failure in acquiring tile {ix}x{iy}, stream {stream.name}.")
def _doMoveAbs(self, f, pos): self._check_hw_error() self._updatePosition() current_pos = self._applyInversion(self.position.value) shifts = {} if f._must_stop.is_set(): raise CancelledError() for axname, val in pos.items(): if self._closed_loop[axname]: shifts[axname] = val - current_pos[axname] encoder_cnts = round(val * self._counts_per_meter[axname]) self.runAbsTargetMove(self._axis_map[axname], encoder_cnts, self._speed_steps[axname]) else: # No absolute move for open-loop => convert to relative move shifts[axname] = val - current_pos[axname] steps_float = shifts[axname] * self._steps_per_meter[axname] steps = int(steps_float) usteps = int((steps_float - steps) * USTEPS_PER_STEP) self.runMotorJog(self._axis_map[axname], steps, usteps, self._speed_steps[axname]) try: self._waitEndMotion(f, shifts) finally: # Leave target mode in case of closed-loop move for ax in pos: self.stopAxis(self._axis_map[ax]) self._updatePosition()
def _doMoveRel(self, f, shift): self._check_hw_error() shifts = {} if f._must_stop.is_set(): raise CancelledError() for axname, val in shift.items(): if self._closed_loop[axname]: shifts[axname] = val encoder_cnts = val * self._counts_per_meter[axname] self.runRelTargetMove(self._axis_map[axname], encoder_cnts, self._speed_steps[axname]) else: shifts[axname] = val steps_float = val * self._steps_per_meter[axname] steps = int(steps_float) usteps = int((steps_float - steps) * USTEPS_PER_STEP) self.runMotorJog(self._axis_map[axname], steps, usteps, self._speed_steps[axname]) try: self._waitEndMotion(f, shifts) finally: # Leave target mode in case of closed-loop move for ax in shift: self.stopAxis(self._axis_map[ax]) self._updatePosition()
def _waitEndMotion(self, f, shifts): """ Wait until move is done. :param f: (CancellableFuture) move future :param shifts: (dict: str --> floats) relative move (in m) between current position and previous position """ dur = 0 for ax, shift in shifts.items(): dur = max(abs(shift / self._speed[ax]), dur) max_dur = dur * 2 + 1 logging.debug("Expecting a move of %g s, will wait up to %g s", dur, max_dur) end_time = time.time() + max_dur moving_axes = set(shifts.keys()) # All axes (still) moving while moving_axes: if f._must_stop.is_set(): for axname in moving_axes: self.stopAxis(self._axis_map[axname]) raise CancelledError() if time.time() > end_time: raise TimeoutError("Timeout after while waiting for end of motion on axes %s for %g s" % (moving_axes, max_dur)) for axname in moving_axes.copy(): # Copy as the set can change during the iteration axis = self._axis_map[axname] if self._closed_loop[axname]: moving = self.isMovingClosedLoop(axis) else: moving = self.isMovingOpenLoop(axis) if not moving: moving_axes.discard(axname) self._check_hw_error() time.sleep(0.05)
def set_executing(self, worker): '''Utility for sub-classes to register the task as executing on a worker.''' if self.cancelled: raise CancelledError() assert not self.pending, '%r pending' % self self.executed_on.append(worker.name) self.attempts += 1 self.signal_start()
def cancel(self): """Send a cancelation to the recipient. TODO: Trio can't do that cleanly. """ if self.scope is not None: self.scope.cancel() self.set_error(CancelledError())
def _acquireStreamCompressedZStack(self, i, ix, iy, stream): """ Acquire a compressed zstack image for the given stream. The method does the following: - Move focus over the list of zlevels - For each focus level acquire image of the stream - Construct xyz cube for the acquired zstack - Compress the cube into a single image using 'maximum intensity projection' :return DataArray: Acquired da for the current tile stream """ zstack = [] for z in self._zlevels: logging.debug(f"Moving focus for tile {ix}x{iy} to {z}.") stream.focuser.moveAbsSync({'z': z}) da = self._acquireStreamTile(i, ix, iy, stream) zstack.append(da) if self._future._task_state == CANCELLED: raise CancelledError() logging.debug( f"Zstack acquisition for tile {ix}x{iy}, stream {stream.name} finished, compressing data into a single image." ) # Convert zstack into a cube fm_cube = assembleZCube(zstack, self._zlevels) # Save the cube on disk if a log path exists if self._log_path: self._save_tiles(ix, iy, fm_cube, stream_cube_id=self._streams.index(stream)) if self._focusing_method == FocusingMethod.MAX_INTENSITY_PROJECTION: # Compress the cube into a single image (using maximum intensity projection) mip_image = numpy.amax(fm_cube, axis=0) if self._future._task_state == CANCELLED: raise CancelledError() logging.debug( f"Zstack compression for tile {ix}x{iy}, stream {stream.name} finished." ) return DataArray(mip_image, copy.copy(zstack[0].metadata)) else: # TODO: support stitched Z-stacks # For now, the init will raise NotImplementedError in such case logging.warning("Zstack returned as-is, while it is not supported") return fm_cube
def _moveTo(self, future, x, y, z, timeout=60): with future._moving_lock: try: if future._must_stop.is_set(): raise CancelledError() logging.debug("Moving to position (%s, %s, %s)", x, y, z) self.parent.MoveStage(x, y, z) # documentation suggests to wait 1s before calling # GetStagePosition() after MoveStage() time.sleep(1) # Wait until the move is over # Don't check for future._must_stop because anyway the stage will # stop moving, and so it's nice to wait until we know the stage is # not moving. moving = True tstart = time.time() while moving: x, y, z, moving = self.parent.GetStagePosition() # Take the opportunity to update .position self._updatePosition({"x": x, "y": y, "z": z}) if time.time() > tstart + timeout: self.parent.Abort() logging.error( "Timeout after submitting stage move. Aborting move." ) break # 50 ms is about the time it takes to read the stage status time.sleep(50e-3) # If it was cancelled, Abort() has stopped the stage before, and # we still have waited until the stage stopped moving. Now let # know the user that the move is not complete. if future._must_stop.is_set(): raise CancelledError() except RemconError: if future._must_stop.is_set(): raise CancelledError() raise finally: future._was_stopped = True # Update the position, even if the move didn't entirely succeed self._updatePosition()
def task(spec, cancel): if cancel.get(): raise CancelledError("Cancelled by request") cov = np.array(spec.inputs['cov']) mean = np.array(spec.inputs['mean']) w = np.array(spec.inputs['c.w'], dtype=float) z = max(0.5, sum(w)) w /= z return {'c.norm': z, 'c.er': mean @ w, 'c.var': w @ cov @ w}
def result(self, timeout=None): self._recv(timeout) if self.state == self.STATE_FINISHED: return self._result elif self.state == self.STATE_EXCEPTION: raise self._exception else: assert self.state == self.STATE_CANCELLED raise CancelledError()
def _moveTo(self, future, pos, timeout=60): with future._moving_lock: try: if future._must_stop.is_set(): raise CancelledError() logging.debug("Moving to position {}".format(pos)) self.parent.move_stage(pos, rel=False) time.sleep(0.5) # Wait until the move is over. # Don't check for future._must_stop because anyway the stage will # stop moving, and so it's nice to wait until we know the stage is # not moving. moving = True tstart = time.time() while moving: pos = self.parent.get_stage_position() moving = self.parent.stage_is_moving() # Take the opportunity to update .position self._updatePosition(pos) if time.time() > tstart + timeout: self.parent.stop_stage_movement() logging.error( "Timeout after submitting stage move. Aborting move." ) break # Wait for 50ms so that we do not keep using the CPU all the time. time.sleep(50e-3) # If it was cancelled, Abort() has stopped the stage before, and # we still have waited until the stage stopped moving. Now let # know the user that the move is not complete. if future._must_stop.is_set(): raise CancelledError() except Exception: if future._must_stop.is_set(): raise CancelledError() raise finally: future._was_stopped = True # Update the position, even if the move didn't entirely succeed self._updatePosition()
def download_installer(self, remote_version): """ Download the installer for the given version to a temporary directory remote_version (str): version number as "1.20.3" return (str): path to the local file """ installer_file = INSTALLER_FILE % remote_version web_file = self._open_remote_file(installer_file) file_size = int(web_file.headers["Content-Length"]) dest_dir = tempfile.gettempdir() local_path = os.path.join(dest_dir, installer_file) logging.info("Downloading from %s (%d bytes) to %s...", web_file.url, file_size, local_path) try: pdlg = wx.ProgressDialog( "Downloading update...", "The new %s installer %s is being downloaded." % (VIEWER_NAME, remote_version), maximum=file_size, parent=wx.GetApp().main_frame, style=wx.PD_CAN_ABORT | wx.PD_AUTO_HIDE | wx.PD_APP_MODAL | wx.PD_REMAINING_TIME) with open(local_path, 'wb') as local_file: count = 0 chunk_size = 100 * 1024 # Too small chunks slows down the download while count < file_size: grabbed = web_file.read(chunk_size) local_file.write(grabbed) if grabbed: count += len(grabbed) else: logging.warning( "Received no more data, will assume the file is only %d bytes", count) break if count > file_size: logging.warning( "Received too much data (%d bytes), will stop", count) break keep_going, skip = pdlg.Update(count) if not keep_going: raise CancelledError("Download cancelled by user") logging.info("Download done.") return local_path finally: try: pdlg.Destroy() except (RuntimeError, AttributeError): pass web_file.close()
def update_tasks(self): """Handles timing out Tasks.""" for task in self.task_manager.timeout_tasks(): self.task_manager.task_done( task.id, TimeoutError("Task timeout", task.timeout)) self.worker_manager.stop_worker(task.worker_id) for task in self.task_manager.cancelled_tasks(): self.task_manager.task_done(task.id, CancelledError()) self.worker_manager.stop_worker(task.worker_id)
def result(self, timeout=None): if self._exception is not None: raise self._exception elif self._cancelled: raise CancelledError() else: if self._result_ready.wait(timeout): return self._result else: raise TimeoutError(f"Timeout of {timeout} seconds exceeded.")
def cancel(self): """Try to cancel a coroutine. Will return True if successfully raised otherwise False""" if self.cancelled: return True if self.complete: return False if self.running(): return False self.cancelled = True self.set_exception( CancelledError("Execution of this coro has been cancelled!")) return True
def _waitEndMove(self, future, axes, end=0): """ Wait until all the given axes are finished moving, or a request to stop has been received. future (Future): the future it handles axes (set of int): the axes IDs to check end (float): expected end time raise: CancelledError: if cancelled before the end of the move """ moving_axes = set(axes) last_upd = time.time() last_axes = moving_axes.copy() try: while not future._must_stop.is_set(): for aid in moving_axes.copy( ): # need copy to remove during iteration if self.IsMotionDone(aid): moving_axes.discard(aid) if not moving_axes: # no more axes to wait for break # Update the position from time to time (10 Hz) if time.time() - last_upd > 0.1 or last_axes != moving_axes: last_names = set(n for n, i in self._name_to_axis.items() if i in last_axes) self._updatePosition(last_names) last_upd = time.time() last_axes = moving_axes.copy() # Wait half of the time left (maximum 0.1 s) left = end - time.time() sleept = max(0.001, min(left / 2, 0.1)) future._must_stop.wait(sleept) # TODO: timeout if really too long else: logging.debug("Move of axes %s cancelled before the end", axes) # stop all axes still moving them for i in moving_axes: self.StopMotion(i) future._was_stopped = True raise CancelledError() except Exception: raise else: # Did everything really finished fine? self._checkError() finally: self._updatePosition() # update (all axes) with final position
def _gather_result(_): result_list = [] for fut in futs: if fut.cancelled(): if return_exceptions: result_list.append(CancelledError()) continue result.set_exception(CancelledError()) break if fut.exception() is None: result_list.append(fut.result()) continue if return_exceptions: result_list.append(fut.exception()) continue result.set_exception(fut.exception()) break result.set_result(result_list)
def _doMoveAbs(self, future, pos): """ Blocking and cancellable absolute move future (Future): the future it handles _pos (dict str -> float): axis name -> absolute target position raise: SmarPodError: if the controller reported an error CancelledError: if cancelled before the end of the move """ last_upd = time.time() dur = 30 # TODO: Calculate an estimated move duration end = time.time() + dur max_dur = dur * 2 + 1 logging.debug("Expecting a move of %g s, will wait up to %g s", dur, max_dur) timeout = last_upd + max_dur with future._moving_lock: self.Move(pos) while not future._must_stop.is_set(): status = self.GetMoveStatus() # check if move is done if status.value == SmarPodDLL.SMARPOD_STOPPED.value: break now = time.time() if now > timeout: logging.warning("Stopping move due to timeout after %g s.", max_dur) self.stop() raise TimeoutError("Move is not over after %g s, while " "expected it takes only %g s" % (max_dur, dur)) # Update the position from time to time (10 Hz) if now - last_upd > 0.1: self._updatePosition() last_upd = time.time() # Wait half of the time left (maximum 0.1 s) left = end - time.time() sleept = max(0.001, min(left / 2, 0.1)) future._must_stop.wait(sleept) else: self.stop() future._was_stopped = True raise CancelledError() self._updatePosition() logging.debug("move successfully completed")
def _search_index(self, f, axname, direction): """ :param f (Future) :param axname (str): axis name (as seen by the user) :param direction (-1 or 1): -1 for negative direction (beginning of the rod), 1 for positive direction returns (bool): True if index was found, false if limit was reached raises PMDError for all other errors except limit exceeded error IOError in case of timeout """ axis = self._axis_map[axname] maxdist = self._axes[axname].range[1] - self._axes[axname].range[ 0] # complete rodlength steps = int(maxdist * self._steps_per_meter[axname]) maxdur = maxdist / self._speed[axname] + 1 end_time = time.time() + 2 * maxdur logging.debug("Searching for index in direction %s.", direction) self.startIndexMode(axis) self.moveToIndex(axis, steps * direction) index_found = False while not index_found: if f._must_stop.is_set(): self.stopAxis(axis) raise CancelledError() # Check for timeout if time.time() > end_time: self.stopAxis(axis) # exit index mode raise IOError( "Timeout while waiting for end of motion on axis %s" % axis) # Check if limit is reached try: self._check_hw_error() except PMDError as ex: if ex.errno == 6: # external limit reached logging.debug("Axis %d limit reached during referencing", axis) self.stopAxis( axis ) # that seems to be necessary after reaching the limit break else: raise # Get index status index_found = self.getIndexStatus(self._axis_map[axname])[-1] time.sleep(0.05) return index_found
def _get_result(future, pipe, timeout): """Waits for result and handles communication errors.""" counter = count(step=SLEEP_UNIT) try: while not pipe.poll(SLEEP_UNIT): if timeout is not None and next(counter) >= timeout: return TimeoutError('Task Timeout', timeout) elif future.cancelled(): return CancelledError() return pipe.recv() except (EOFError, OSError): return ProcessExpired('Abnormal termination') except Exception as error: return error
def main(spec, cancel): out = err = None with run_it(spec) as proc: while True: if cancel.get(): raise CancelledError("Cancelled by request") try: out, err = proc.communicate(timeout=5) except sp.TimeoutExpired: continue if proc.returncode: raise RuntimeError( ("Subprocess exited with status %s. stdout:\n%s\n" + "stderr:\n%s") % (proc.returncode, out, err)) else: return {"sum": int(out), "warnings": err}
def _doReference(self, future, axes): """ Actually runs the referencing code axes (set of str) raise: IOError: if referencing failed due to hardware CancelledError if was cancelled """ # Reset reference so that if it fails, it states the axes are not # referenced (anymore) with future._moving_lock: try: # do the referencing for each axis sequentially # (because each referencing is synchronous) for a in axes: if future._must_stop.is_set(): raise CancelledError() aid = self._axis_map[a] self.referenced._value[a] = False self.HomeSearch( aid, REF_NEGATIVE_LIMIT ) # search for the negative limit signal to set an origin self._waitEndMove(future, (aid, ), time.time() + 100) # block until it's over self.SetHome(aid, 0.0) # set negative limit as origin self.referenced._value[a] = True except CancelledError: # FIXME: if the referencing is stopped, the device refuses to # move until referencing is run (and successful). # => Need to put back the device into a mode where at least # relative moves work. logging.warning( "Referencing cancelled, device will not move until another referencing" ) future._was_stopped = True raise except Exception: logging.exception("Referencing failure") raise finally: # We only notify after updating the position so that when a listener # receives updates both values are already updated. self._updatePosition( axes) # all the referenced axes should be back to 0 # read-only so manually notify self.referenced.notify(self.referenced.value)
def acquire_roa(self, dataflow): """ Acquire the single field images that resemble the region of acquisition (ROA, megafield image). :param dataflow: (model.DataFlow) The dataflow on the detector. :return: (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). """ total_field_time = self._detector.frameDuration.value timeout = total_field_time + 5 # TODO what margin should be used? # Acquire all single field images, which are automatically offloaded to the external storage. for field_idx in self._roa.field_indices: # Reset the event that waits for the image being received (puts flag to false). self._data_received.clear() self.field_idx = field_idx logging.debug("Acquiring field with index: %s", field_idx) self.move_stage_to_next_tile( ) # move stage to next field image position if field_idx != (0, 0): self.correct_beam_shift( ) # correct the shift of the beams caused by the parasitic magnetic field. dataflow.next(field_idx) # acquire the next field image. # Wait until single field image data has been received (image_received sets flag to True). if not self._data_received.wait(timeout): # TODO here we often timeout when actually just the offload queue is full # need to handle offload queue error differently to just wait a bit instead of timing out # -> check if finish megafield is called in finally when hitting here raise TimeoutError("Timeout while waiting for field image.") self._fields_remaining.discard(field_idx) # In case the acquisition was cancelled by a client, before the future returned, raise cancellation error. # Note: The acquisition of the current single field image (tile) is still finished though. if self._cancelled: raise CancelledError() # Update the time left for the acquisition. expected_time = len(self._fields_remaining) * total_field_time self._future.set_progress(end=time.time() + expected_time) return self.megafield
def _doReference(self, f, axes): self._check_hw_error() # Request referencing on all axes # Referencing procedure: index signal is in the middle of the rod (when using encoder) # * move to the limit switch in the negative direction (fixed end of the rod) # * once we reach the limit switch, PMD error 6 will be raised # * move back in the opposite direction until indexing signal is registered # In case there is no encoder, it is still possible to reference. The motor has an internal indexing # signal 8.9 µm from fixed end of the rod (the end of the rod should be the most negative position if # the axis is not inverted). By referencing, this position can be set to 0 and absolute moves are possible # (although with less accuracy). However, we do not currently support this type of referencing without # encoder. With our hardware attached to the motor, it is impossible to reach the 8.9 µm indexing position. for axname in axes: if f._must_stop.is_set(): self.stopAxis(self._axis_map[axname]) raise CancelledError() axis = self._axis_map[axname] self.referenced._value[axname] = False # First, search for the index in negative direction. idx_found = self._search_index(f, axname, direction=-1) # If it wasn't found, try again in positive direction. if not idx_found: logging.debug("Referencing axis %s in the positive direction", axis) idx_found = self._search_index(f, axname, direction=1) # If it's still not found, something went wrong. if not idx_found: raise ValueError( "Couldn't find index on axis %s (%s), referencing failed." % (axis, axname)) # Referencing complete logging.debug("Finished referencing axis %s." % axname) self.stopAxis( axis ) # the axis should already be stopped, make sure for safety self.referenced._value[axname] = True # read-only so manually notify self.referenced.notify(self.referenced.value) self._updatePosition()
def acquire_roa(self, dataflow): """ Acquire the single field images that resemble the region of acquisition (ROA, megafield image). :param dataflow: (model.DataFlow) The dataflow on the detector. """ total_field_time = self._detector.frameDuration.value + 1.5 # there is about 1.5 seconds overhead per field # The first field is acquired twice, so the timeout must be at least twice the total field time. # Use 5 times the total field time to have a wide margin. timeout = 5 * total_field_time + 2 # Acquire all single field images, which are automatically offloaded to the external storage. for field_idx in self._roa.field_indices: # Reset the event that waits for the image being received (puts flag to false). self._data_received.clear() self.field_idx = field_idx logging.debug("Acquiring field with index: %s", field_idx) self.move_stage_to_next_tile( ) # move stage to next field image position self.correct_beam_shift( ) # correct the shift of the beams caused by the parasitic magnetic field. dataflow.next(field_idx) # acquire the next field image. # Wait until single field image data has been received (image_received sets flag to True). if not self._data_received.wait(timeout): # TODO here we often timeout when actually just the offload queue is full # need to handle offload queue error differently to just wait a bit instead of timing out # -> check if finish megafield is called in finally when hitting here raise TimeoutError("Timeout while waiting for field image.") self._fields_remaining.discard(field_idx) # In case the acquisition was cancelled by a client, before the future returned, raise cancellation error. # Note: The acquisition of the current single field image (tile) is still finished though. if self._cancelled: raise CancelledError() # Update the time left for the acquisition. expected_time = len(self._fields_remaining) * total_field_time self._future.set_progress(start=time.time(), end=time.time() + expected_time) logging.debug("Successfully acquired all fields of ROA.")
def exception(self, timeout: int = None): start = time.time() state, error = self._get_state_and_error() while state.is_executing(): if timeout is not None and (time.time() - start) > timeout: raise TimeoutError( f"{self.job_id} did not finish running before timeout of {timeout}s" ) time.sleep(10) state, error = self._get_state_and_error() logging.info(f"Future for {self.job_id} has state {state}") if state.is_cancelled(): raise CancelledError(f"{self.job_id}: " + error) if state is State.FAILED: return AipError(f"{self.job_id}: " + error)
def _adjustFocus(self, das, i, ix, iy): if i % SKIP_TILES != 0: logging.debug("Skipping focus adjustment..") return das try: current_focus_level = MeasureOpticalFocus(das[self._streams.index( self._focus_stream)]) except IndexError: logging.warning("Failed to get image to measure focus on.") return das if i == 0: # Use initial optical focus level to be compared to next tiles # TODO: instead of using the first image, use the best 10% images (excluding outliers) self._good_focus_level = current_focus_level # TODO: handle the case of _good_focus_level == 0 logging.debug("Current focus level: %s (good = %s)", current_focus_level, self._good_focus_level) # Run autofocus if current focus got worse than permitted deviation if abs(current_focus_level - self._good_focus_level ) / self._good_focus_level > FOCUS_FIDELITY: try: self._future.running_subf = AutoFocus( self._focus_stream.detector, self._focus_stream.emitter, self._focus_stream.focuser, good_focus=self._good_focus, rng_focus=self._focus_rng, method=MTD_EXHAUSTIVE) self._future.running_subf.result( ) # blocks until autofocus is finished if self._future._task_state == CANCELLED: raise CancelledError() except CancelledError: raise except Exception as ex: logging.exception("Running autofocus failed on image i= %s." % i) else: # Reacquire the out of focus tile (which should be corrected now) das = self._getTileDAs(i, ix, iy) return das