async def set_rpm(activate): if activate: rpm = config["low_temperature"]["rpm_cold"] log.warning(f"Low temperature mode. Setting RPM={rpm}.") else: rpm = config["low_temperature"]["rpm_normal"] log.warning( f"Disabling low temperature mode. Setting RPM={rpm}.") config["positioner"]["motor_speed"] = rpm
async def set_idle_power(activate): if activate: ht = config["low_temperature"]["holding_torque_very_cold"] log.warning( "Very low temperature mode. Setting holding torque.") else: ht = config["low_temperature"]["holding_torque_normal"] log.warning( "Disabling very low temperature mode. Setting holding torque." ) await self.send_command( CommandID.SET_HOLDING_CURRENT, alpha=ht[0], beta=ht[1], )
async def unwind( current_positions: dict[int, tuple[float | None, float | None]], collision_buffer: float | None = None, disabled: list[int] = [], force: bool = False, ): """Folds all the robots to the lattice position. This coroutine uses a process pool executor to run Kaiju routines. """ alpha0, beta0 = config["kaiju"]["lattice_position"] # We create the data directly since it's simple. This should be a bit faster # than creating a grid and dumping it. data = {"collision_buffer": collision_buffer, "grid": {}} for pid, (alpha, beta) in current_positions.items(): data["grid"][int(pid)] = (alpha, beta, alpha0, beta0, pid in disabled) (to_destination, _, did_fail, deadlocks) = await run_in_executor( get_path_pair, data=data, ignore_did_fail=force, stop_if_deadlock=force, executor="process", ) if did_fail: if force is False: raise TrajectoryError( "Failed generating a valid unwind trajectory. " f"{len(deadlocks)} deadlocks were found ({deadlocks}).") else: log.warning("Deadlocks found in unwind but proceeding anyway.") return to_destination
async def update_status( self, positioner_ids: Optional[int | List[int]] = None, timeout: float = 1, ) -> bool: """Update statuses for all positioners. Parameters ---------- positioner_ids The list of positioners to update. If `None`, update all positioners. timeout How long to wait before timing out the command. """ if len(self.positioners) == 0: return True if positioner_ids is None: positioner_ids = [0] elif not isinstance(positioner_ids, (list, tuple)): positioner_ids = [positioner_ids] if positioner_ids == [0]: n_positioners = len(self) if len(self) > 0 else None else: n_positioners = None await self.update_firmware_version(timeout=timeout) command = self.send_command( CommandID.GET_STATUS, positioner_ids=positioner_ids, n_positioners=n_positioners, timeout=timeout, ) await command if command.status.failed: log.warning( f"{CommandID.GET_STATUS.name!r} failed during update status.") return False if len(command.replies) == 0: return True update_status_coros = [] for pid, status_int in command.get_positioner_status().items( ): # type: ignore if pid not in self: continue update_status_coros.append(self[pid].update_status(status_int)) await asyncio.gather(*update_status_coros) # Set the status of the FPS based on positioner information. # First get the current bitmask without the status bit. current = self.status & ~FPSStatus.STATUS_BITS pbits = numpy.array([int(p.status) for p in self.values()]) coll_bits = PositionerStatus.COLLISION_ALPHA | PositionerStatus.COLLISION_BETA if ((pbits & coll_bits) > 0).any(): self.set_status(current | FPSStatus.COLLIDED) elif ((pbits & PositionerStatus.DISPLACEMENT_COMPLETED) > 0).all(): self.set_status(current | FPSStatus.IDLE) else: self.set_status(current | FPSStatus.MOVING) return True
async def _handle_temperature(self): """Handle positioners in low temperature.""" if not isinstance(self.ieb, IEB): log.error("Cannot handle low-temperature mode. IEB not present.") async def set_rpm(activate): if activate: rpm = config["low_temperature"]["rpm_cold"] log.warning(f"Low temperature mode. Setting RPM={rpm}.") else: rpm = config["low_temperature"]["rpm_normal"] log.warning( f"Disabling low temperature mode. Setting RPM={rpm}.") config["positioner"]["motor_speed"] = rpm async def set_idle_power(activate): if activate: ht = config["low_temperature"]["holding_torque_very_cold"] log.warning( "Very low temperature mode. Setting holding torque.") else: ht = config["low_temperature"]["holding_torque_normal"] log.warning( "Disabling very low temperature mode. Setting holding torque." ) await self.send_command( CommandID.SET_HOLDING_CURRENT, alpha=ht[0], beta=ht[1], ) sensor = config["low_temperature"]["sensor"] cold = config["low_temperature"]["cold_threshold"] very_cold = config["low_temperature"]["very_cold_threshold"] interval = config["low_temperature"]["interval"] while True: try: assert isinstance(self.ieb, IEB) and self.ieb.disabled is False device = self.ieb.get_device(sensor) temp = (await device.read())[0] # Get the status without the temperature bits. base_status = self.status & ~FPSStatus.TEMPERATURE_BITS if temp <= very_cold: if self.status & FPSStatus.TEMPERATURE_NORMAL: await set_rpm(True) await set_idle_power(True) elif self.status & FPSStatus.TEMPERATURE_COLD: await set_idle_power(True) else: pass self.set_status(base_status | FPSStatus.TEMPERATURE_VERY_COLD) elif temp <= cold: if self.status & FPSStatus.TEMPERATURE_NORMAL: await set_rpm(True) elif self.status & FPSStatus.TEMPERATURE_COLD: pass else: await set_idle_power(False) self.set_status(base_status | FPSStatus.TEMPERATURE_COLD) else: if self.status & FPSStatus.TEMPERATURE_NORMAL: pass elif self.status & FPSStatus.TEMPERATURE_COLD: await set_rpm(False) else: await set_rpm(False) await set_idle_power(False) self.set_status(base_status | FPSStatus.TEMPERATURE_NORMAL) except BaseException as err: log.warning( f"Cannot read device {sensor!r}. " f"Low-temperature tracking temporarily disabled: {err}", ) base_status = self.status & ~FPSStatus.TEMPERATURE_BITS self.set_status(base_status | FPSStatus.TEMPERATURE_UNKNOWN) finally: await asyncio.sleep(interval)
async def apply_correction( self, offsets: Optional[pandas.DataFrame] = None, ): # pragma: no cover """Applies the offsets. Fails if the trajectory is collided or deadlock.""" if self.fps.locked: raise FVCError("The FPS is locked. Cannot apply corrections.") self.log("Preparing correction trajectory.") if self.offsets is None and offsets is None: raise FVCError( "Offsets not set or passed. Cannot apply correction.") if offsets is None: offsets = self.offsets assert offsets is not None await self.fps.update_position() target_distance = config["fvc"]["target_distance"] # Setup robot grid. grid = get_robot_grid(self.fps) for robot in grid.robotDict.values(): if robot.isOffline: continue positioner = self.fps[robot.id] robot.setAlphaBeta(positioner.alpha, positioner.beta) robot.setDestinationAlphaBeta(positioner.alpha, positioner.beta) if offsets.loc[robot.id].transformation_valid == 0: continue new = offsets.loc[robot.id, ["alpha_new", "beta_new"]] dist = offsets.loc[robot.id, ["xwok_distance", "ywok_distance"]] * 1000.0 if numpy.hypot(dist.xwok_distance, dist.ywok_distance) > target_distance: robot.setDestinationAlphaBeta(new.alpha_new, new.beta_new) else: robot.isOffline = True # Check for collisions. If robots are collided just leave them there. collided = grid.getCollidedRobotList() n_coll = len(collided) if n_coll > 0: for pid in collided: positioner = self.fps[pid] alpha = positioner.alpha beta = positioner.beta grid.robotDict[pid].setAlphaBeta(alpha, beta) grid.robotDict[pid].setDestinationAlphaBeta(alpha, beta) # Generate trajectories. (to_destination, _, did_fail, deadlocks) = await get_path_pair_in_executor( grid, ignore_did_fail=True, stop_if_deadlock=True, ignore_initial_collisions=True, ) if did_fail: log.warning( f"Found {len(deadlocks)} deadlocks but applying correction anyway." ) self.log("Sending correction trajectory.") try: await self.fps.send_trajectory(to_destination, command=self.command) except TrajectoryError as err: raise FVCError( f"Failed executing the correction trajectory: {err}") self.correction_applied = True self.log("Correction applied.")
async def calibrate_positioner( fps, positioner_id, motors=True, datums=True, cogging=True ): """Runs the calibration process and saves it to the internal memory. Parameters ---------- fps : .FPS The instance of `.FPS` that will receive the trajectory. positioner_id : int The ID of the positioner to calibrate. motors : bool Whether to perform the motor calibration. datums : bool Whether to perform the datums calibration. cogging : bool Whether to perform the cogging calibration (may take more than one hour). Raises ------ JaegerError If encounters a problem during the process. Examples -------- :: >>> fps = FPS() >>> await fps.initialise() # Calibrate positioner 31. >>> await calibrate_positioner(fps, 31) """ log.info(f"Calibrating positioner {positioner_id}.") if positioner_id not in fps.positioners: raise JaegerError(f"Positioner {positioner_id} not found.") positioner = fps[positioner_id] if fps.pollers.running: log.debug("Stopping pollers") await fps.pollers.stop() if motors: log.info("Starting motor calibration.") cmd = await fps.send_command( CommandID.START_MOTOR_CALIBRATION, positioner_ids=positioner_id, ) if cmd.status.failed: raise JaegerError("Motor calibration failed.") await asyncio.sleep(1) await positioner.wait_for_status( [ PS.DISPLACEMENT_COMPLETED, PS.MOTOR_ALPHA_CALIBRATED, PS.MOTOR_BETA_CALIBRATED, ] ) else: log.warning("Skipping motor calibration.") if datums: log.info("Starting datum calibration.") cmd = await fps.send_command( CommandID.START_DATUM_CALIBRATION, positioner_ids=positioner_id, ) if cmd.status.failed: raise JaegerError("Datum calibration failed.") await asyncio.sleep(1) await positioner.wait_for_status( [ PS.DISPLACEMENT_COMPLETED, PS.DATUM_ALPHA_CALIBRATED, PS.DATUM_BETA_CALIBRATED, ] ) else: log.warning("Skipping datum calibration.") if cogging: log.info("Starting cogging calibration.") cmd = await fps.send_command( CommandID.START_COGGING_CALIBRATION, positioner_ids=positioner_id, ) if cmd.status.failed: raise JaegerError("Cogging calibration failed.") await asyncio.sleep(1) await positioner.wait_for_status( [PS.COGGING_ALPHA_CALIBRATED, PS.COGGING_BETA_CALIBRATED] ) else: log.warning("Skipping cogging calibration.") if motors or datums or cogging: log.info("Saving calibration.") cmd = await fps.send_command( CommandID.SAVE_INTERNAL_CALIBRATION, positioner_ids=positioner_id, ) if cmd.status.failed: raise JaegerError("Saving calibration failed.") log.info(f"Positioner {positioner_id} has been calibrated.") return
async def create_random_configuration( fps: FPS, seed: int | None = None, safe=True, uniform: tuple[float, ...] | None = None, collision_buffer: float | None = None, max_deadlocks: int = 6, deadlock_retries: int = 5, **kwargs, ): """Creates a random configuration using Kaiju.""" from jaeger.target.configuration import ManualConfiguration seed = seed or numpy.random.randint(0, 1000000) numpy.random.seed(seed) robot_grid = get_robot_grid(fps, seed=seed, collision_buffer=collision_buffer) alphaL, betaL = config["kaiju"]["lattice_position"] # We use Kaiju for convenience in the non-safe mode. for robot in robot_grid.robotDict.values(): if robot.isOffline: continue if uniform is not None: alpha0, alpha1, beta0, beta1 = uniform robot.setAlphaBeta( numpy.random.uniform(alpha0, alpha1), numpy.random.uniform(beta0, beta1), ) else: if safe: safe_mode = config["safe_mode"] if safe_mode is False: safe_mode = {"min_beta": 165, "max_beta": 175} robot.setAlphaBeta( numpy.random.uniform(0, 359.9), numpy.random.uniform( safe_mode["min_beta"], 175.0, ), ) else: robot.setXYUniform() robot.setDestinationAlphaBeta(alphaL, betaL) # Confirm that the configuration is valid. This should only matter # for full range random configurations. try: robot_grid, _ = await decollide_in_executor(robot_grid, simple=True) grid_data = { robot.id: (robot.alpha, robot.beta) for robot in robot_grid.robotDict.values() } except JaegerError: raise JaegerError( "Decollision failed. Cannot create random configuration.") _, _, did_fail, deadlocks = await get_path_pair_in_executor(robot_grid) # If too many deadlocks, just try a new seed. n_deadlock = len(deadlocks) if did_fail and n_deadlock > max_deadlocks: log.warning("Too many deadlocked robots. Trying new seed.") return await create_random_configuration( fps, safe=safe, uniform=uniform, collision_buffer=collision_buffer, deadlock_retries=deadlock_retries, ) if did_fail and n_deadlock > 0: # Now the fun part, if there are only a few deadlocks, try assigning them # a random position. log.warning(f"Found {n_deadlock} deadlocked robots. Trying to unlock.") for nn in range(1, deadlock_retries + 1): log.info(f"Retry {nn} out of {deadlock_retries}.") to_replace_robot = numpy.random.choice(deadlocks) robot_grid = get_robot_grid( fps, seed=seed + 1, collision_buffer=collision_buffer, ) for robot in robot_grid.robotDict.values(): if robot.isOffline: continue if robot.id == to_replace_robot: robot.setXYUniform() else: robot.setAlphaBeta(*grid_data[robot.id]) try: robot_grid, _ = await decollide_in_executor(robot_grid, simple=True) grid_data = { robot.id: (robot.alpha, robot.beta) for robot in robot_grid.robotDict.values() } except JaegerError: raise JaegerError( "Decollision failed. Cannot create random configuration.") _, _, did_fail, deadlocks = await get_path_pair_in_executor( robot_grid) if did_fail is False: log.info("Random configuration has been unlocked.") break else: log.info(f"{len(deadlocks)} deadlocks remaining.") if nn == deadlock_retries: log.warning("Failed unlocking. Trying new seed.") return await create_random_configuration( fps, seed=seed + 1, safe=safe, uniform=uniform, collision_buffer=collision_buffer, deadlock_retries=deadlock_retries, ) pT = calibration.positionerTable.copy().reset_index() # Build an assignment dictionary. data = {} for pid in grid_data: holeID = pT.loc[pT.positionerID == pid].holeID.values[0] data[holeID] = { "alpha": grid_data[pid][0], "beta": grid_data[pid][1], "fibre_type": "Metrology", } return ManualConfiguration(data, **kwargs)