async def explode( fps_maker, explode_deg: float, one: int | None = None, ): """Explodes the array.""" from jaeger.kaiju import explode async with fps_maker as fps: if fps.locked: FPSLockedError("The FPS is locked.") await fps.update_position() positions = {p.positioner_id: (p.alpha, p.beta) for p in fps.values()} log.info("Calculating trajectory.") trajectory = await explode( positions, explode_deg=explode_deg, disabled=[ pid for pid in fps.positioners if fps.positioners[pid].disabled ], positioner_id=one, ) log.info("Executing explode trajectory.") await fps.send_trajectory(trajectory) return
async def shutdown(self): """Stops pollers and shuts down all remaining tasks.""" if not self.is_bootloader: log.info("Stopping positioners and shutting down.") await self.stop_trajectory() log.debug("Stopping all pollers.") if self.pollers: await self.pollers.stop() await self.chiller.stop() await self.alerts.stop() log.debug("Cancelling all pending tasks and shutting down.") tasks = [ task for task in asyncio.all_tasks(loop=self.loop) if task is not asyncio.current_task(loop=self.loop) ] list(map(lambda task: task.cancel(), tasks)) await asyncio.gather(*tasks, return_exceptions=True) self.loop.stop() del self.__class__._instance[self.__class__]
async def unwind(fps_maker, collision_buffer: float | None = None, force: bool = False): """Unwinds the array.""" from jaeger.kaiju import unwind async with fps_maker as fps: if fps.locked: FPSLockedError("The FPS is locked.") await fps.update_position() positions = {p.positioner_id: (p.alpha, p.beta) for p in fps.values()} log.info("Calculating trajectory.") trajectory = await unwind( positions, collision_buffer=collision_buffer, disabled=[ pid for pid in fps.positioners if fps.positioners[pid].disabled ], force=force, ) log.info("Executing unwind trajectory.") await fps.send_trajectory(trajectory) return
async def main(): fps = await FPS.create() assert len(fps) > 0, "No positioners connected." n_points = int(sys.argv[1]) if len(sys.argv) > 1 else 1 alpha_points = numpy.linspace(0, 180, n_points + 1) beta_points = numpy.linspace(180, 180, n_points + 1) t_points = numpy.linspace(1, 30, n_points + 1) alpha = list(zip(alpha_points, t_points)) beta = list(zip(beta_points, t_points)) log.info("Going to start position.") await asyncio.gather(*[fps[pid].goto(0, 180) for pid in fps]) log.info("Sending trajectory.") await fps.send_trajectory( {pid: { "alpha": alpha, "beta": beta } for pid in fps}, use_sync_line=False, )
async def set_positions(fps_maker, positioner_id, alpha, beta): """Sets the position of the alpha and beta arms.""" if alpha < 0 or alpha >= 360: raise click.UsageError("alpha must be in the range [0, 360)") if beta < 0 or beta >= 360: raise click.UsageError("beta must be in the range [0, 360)") async with fps_maker as fps: positioner = fps.positioners[positioner_id] result = await positioner.set_position(alpha, beta) if not result: log.error("failed to set positions.") return log.info(f"positioner {positioner_id} set to {(alpha, beta)}.")
async def load_firmware( fps: FPS, firmware_file: str | pathlib.Path, positioners: Optional[List[int]] = None, messages_per_positioner: Optional[int] = None, force: bool = False, show_progressbar: bool = False, progress_callback: Optional[Callable[[int, int], Any]] = None, stop_logging: bool = True, ): """Convenience function to run through the steps of loading a new firmware. This function is a coroutine and not intendend for direct use. Use the ``jaeger`` CLI instead. Parameters ---------- fps `~jaeger.fps.FPS` instance to which the commands will be sent. firmware_file Binary file containing the firmware to load. positioners A list of positioner ids whose firmware to update, or `None` to update all the positioners in ``fps``. messages_per_positioner How many messages to send to each positioner at once. This can improve the performance but also overflow the CAN bus buffer. With the default value of `None`, reverts to the configuration value ``positioner.firmware_messages_per_positioner``. force Forces the firmware load to continue even if some positioners are not responding or are not in bootloader mode. show_progressbar Whether to show a progress bar. progress_callback A function to call as data gets transferred to the positioners. The callback is called with ``(current_chunk, n_chuck)`` where ``current_chunk`` is the number of the data chunk being sent and ``n_chunk`` is the total number of chunks in the data package. stop_logging Disable logging to file for the CAN logger to improve performance. """ if show_progressbar: try: import progressbar except ImportError: warnings.warn( "progressbar2 is not installed. Cannot show a progress bar.", JaegerUserWarning, ) progressbar = None show_progressbar = False else: progressbar = None start_time = time.time() firmware_file = pathlib.Path(firmware_file) assert firmware_file.exists(), "cannot find firmware file" log.info(f"firmware file {firmware_file!s} found.") # Open firmware data as binary. firmware_data = open(firmware_file, "rb") crc32 = zlib.crc32(firmware_data.read()) filesize = os.path.getsize(firmware_file) # Check to make sure all positioners are in bootloader mode. valid_positioners = [] n_bad = 0 for positioner_id in fps.positioners: if positioners is not None and positioner_id not in positioners: continue positioner = fps.positioners[positioner_id] if (not positioner.is_bootloader() or BootloaderStatus.BOOTLOADER_INIT not in positioner.status or BootloaderStatus.UNKNOWN in positioner.status): n_bad += 1 continue valid_positioners.append(positioner) if len(valid_positioners) == 0: raise JaegerError( "no positioners found in bootloader mode or with valid status.") if n_bad > 0: msg = f"{n_bad} positioners not in bootloader mode or state is invalid." if force: warnings.warn(msg + " Proceeding becasuse force=True.", JaegerUserWarning) else: raise JaegerError(msg) log.info("stopping pollers") await fps.pollers.stop() log.info(f"upgrading firmware on {len(valid_positioners)} positioners.") start_firmware_payload = int_to_bytes(filesize) + int_to_bytes(crc32) log.info(f"CRC32: {crc32}") log.info(f"File size: {filesize} bytes") pids = [pos.positioner_id for pos in valid_positioners] cmd = await fps.send_command( CommandID.START_FIRMWARE_UPGRADE, positioner_ids=pids, data=[start_firmware_payload], ) if cmd.status.failed or cmd.status.timed_out: log.error("firmware upgrade failed.") return False # Restore pointer to start of file firmware_data.seek(0) log.info("starting data send.") if stop_logging and can_log.fh: fh_handler = can_log.handlers.pop(can_log.handlers.index(can_log.fh)) else: fh_handler = None chunk_size = 8 n_chunks = int(numpy.ceil(filesize / chunk_size)) with contextlib.ExitStack() as stack: if show_progressbar and progressbar: bar = stack.enter_context( progressbar.ProgressBar(max_value=n_chunks)) else: bar = None messages_default = config["positioner"][ "firmware_messages_per_positioner"] messages_per_positioner = messages_per_positioner or messages_default assert isinstance(messages_per_positioner, int) ii = 0 while True: cmds = [] stop = False for __ in range(messages_per_positioner): chunk = firmware_data.read(chunk_size) packetdata = bytearray(chunk) # packetdata.reverse() # IMPORTANT! no longer needed for P1 if len(packetdata) == 0: stop = True break cmds.append( fps.send_command( CommandID.SEND_FIRMWARE_DATA, positioner_ids=pids, data=[packetdata], timeout=15, )) await asyncio.gather(*cmds) if any(cmd.status.failed or cmd.status.timed_out for cmd in cmds): log.error("firmware upgrade failed.") if fh_handler: can_log.addHandler(fh_handler) return False ii += messages_per_positioner if show_progressbar and bar: if ii < n_chunks: bar.update(ii) if progress_callback: progress_callback(ii, n_chunks) if stop: break log.info("firmware upgrade complete.") if fh_handler: can_log.addHandler(fh_handler) total_time = time.time() - start_time log.info(f"upgrading firmware took {total_time:.2f} s.") return True
async def upgrade_firmware( fps_maker, firmware_file, force, positioners, no_cycle, sextants, all_on, yes, ): """Upgrades the firmaware.""" if positioners is not None: positioners = [ int(positioner.strip()) for positioner in positioners.split(",") ] fps_maker.initialise = False sextants = sextants or (1, 2, 3, 4, 5, 6) if not yes: click.confirm( f"Upgrade firmware for sextant(s) {', '.join(map(str, sextants))}?", default=False, abort=True, ) async with fps_maker as fps: ps_devs = [] if fps.ieb and no_cycle is False: for module in fps.ieb.modules.values(): for dev_name in module.devices: if dev_name.upper().startswith("PS"): ps_devs.append(dev_name) for sextant in sextants: log.info(f"Upgrading firmware on sextant {sextant}.") if fps.ieb and no_cycle is False: log.info("Power cycling positioners") for dev in ps_devs: await fps.ieb.get_device(dev).open() await asyncio.sleep(1) await asyncio.sleep(5) dev = "PS" + str(sextant) await fps.ieb.get_device(dev).close() await asyncio.sleep(3) await fps.initialise(start_pollers=False) await load_firmware( fps, firmware_file, positioners=positioners, force=force, show_progressbar=True, ) if all_on is True: log.info("Powering on sextants.") for sextant in sextants: await fps.ieb.get_device(f"PS{sextant}").close() await asyncio.sleep(3)
async def initialise( self: T, start_pollers: bool | None = None, enable_low_temperature: bool = True, ) -> T: """Initialises all positioners with status and firmware version. Parameters ---------- start_pollers Whether to initialise the pollers. """ if start_pollers is None: start_pollers = config["fps"]["start_pollers"] assert isinstance(start_pollers, bool) # Clear all robots self.clear() self.positioner_to_bus = {} # Stop pollers while initialising if self.pollers.running: await self.pollers.stop() # Make sure CAN buses are connected. await self.start_can() # Test IEB connection. if isinstance(self.ieb, IEB): try: async with self.ieb: pass except BaseException as err: warnings.warn(str(err), JaegerUserWarning) assert isinstance(self.can, JaegerCAN), "CAN connection not established." if len(self.can.interfaces) == 0: warnings.warn("CAN interfaces not found.", JaegerUserWarning) return self # Get the positioner-to-bus map await self._get_positioner_bus_map() # Stop poller in case they are running await self.pollers.stop() get_fw_command = self.send_command( CommandID.GET_FIRMWARE_VERSION, positioner_ids=0, timeout=config["fps"]["initialise_timeouts"], ) assert isinstance(get_fw_command, GetFirmwareVersion) await get_fw_command if get_fw_command.status.failed: raise JaegerError("Failed retrieving firmware version.") # Loops over each reply and set the positioner status to OK. If the # positioner was not in the list, adds it. for reply in get_fw_command.replies: if reply.positioner_id not in self.positioners: if hasattr(reply.message, "interface"): interface = reply.message.interface bus = reply.message.bus else: interface = bus = None self.add_positioner(reply.positioner_id, interface=interface, bus=bus) positioner = self.positioners[reply.positioner_id] positioner.fps = self positioner.firmware = get_fw_command.get_firmware()[ reply.positioner_id] if positioner.positioner_id in config["fps"][ "disabled_positioners"]: positioner.disabled = True # Add offline robots. Offline positioners are physically in the array but # they don't reply to commands and we need to specify their position. Once # That's done they behave as normal disabled robots. if config["fps"]["offline_positioners"] is not None: for pid in config["fps"]["offline_positioners"]: off_alpha, off_beta = config["fps"]["offline_positioners"][pid] if pid not in self.positioners: positioner = self.add_positioner(pid) else: positioner = self.positioners[pid] positioner.disabled = True positioner.offline = True positioner.alpha = off_alpha positioner.beta = off_beta # Mark as initialised here although we have some more work to do. self.initialised = True pids = sorted(list(self.keys())) if len(pids) > 0: log.info(f"Found {len(pids)} connected positioners: {pids!r}.") else: warnings.warn("No positioners found.", JaegerUserWarning) return self if len(set([pos.firmware for pos in self.values()])) > 1: warnings.warn( "Found positioners with different firmware versions.", JaegerUserWarning, ) # Stop all positioners just in case. This won't clear collided flags. if not self.is_bootloader(): await self.stop_trajectory() # Initialise positioners try: disable_precise_moves = config["positioner"][ "disable_precise_moves"] # if disable_precise_moves: # warnings.warn("Disabling precise moves.", JaegerUserWarning) pos_initialise = [ positioner.initialise( disable_precise_moves=disable_precise_moves) for positioner in self.values() ] await asyncio.gather(*pos_initialise) except (JaegerError, PositionerError) as err: raise JaegerError(f"Some positioners failed to initialise: {err}") if disable_precise_moves is True and any( [self[i].precise_moves for i in self]): log.error("Unable to disable precise moves for some positioners.") n_non_initialised = len([ positioner for positioner in self.positioners.values() if (positioner.status == positioner.flags.UNKNOWN or not positioner.initialised) ]) if n_non_initialised > 0: raise JaegerError( f"{n_non_initialised} positioners failed to initialise.") if self.is_bootloader(): bootlist = [ p.positioner_id for p in self.values() if p.is_bootloader() ] warnings.warn( f"Positioners in booloader mode: {bootlist!r}.", JaegerUserWarning, ) return self # Check if any of the positioners are collided and if so lock the FPS. locked_by = [] for positioner in self.values(): if positioner.collision: locked_by.append(positioner.positioner_id) if len(locked_by) > 0: await self.lock(by=locked_by, do_warn=False, snapshot=False) warnings.warn( "The FPS was collided and has been locked.", JaegerUserWarning, ) if config.get("safe_mode", False) is not False: min_beta = MIN_BETA if isinstance(config["safe_mode"], dict): min_beta = config["safe_mode"].get("min_beta", MIN_BETA) warnings.warn( f"Safe mode enabled. Minimum beta is {min_beta} degrees.", JaegerUserWarning, ) # Disable collision detection for listed robots. disable_collision = config["fps"][ "disable_collision_detection_positioners"] disable_connected = list( set(disable_collision) & set(self.positioners.keys())) if len(disable_connected) > 0: warnings.warn( f"Disabling collision detection for positioners {disable_connected}.", JaegerUserWarning, ) await self.send_command( CommandID.ALPHA_CLOSED_LOOP_WITHOUT_COLLISION_DETECTION, positioner_ids=disable_connected, ) await self.send_command( CommandID.BETA_CLOSED_LOOP_WITHOUT_COLLISION_DETECTION, positioner_ids=disable_connected, ) # Start temperature watcher. if self.__temperature_task is not None: self.__temperature_task.cancel() if (isinstance(self.ieb, IEB) and not self.ieb.disabled and enable_low_temperature): self.__temperature_task = asyncio.create_task( self._handle_temperature()) else: self.set_status((self.status & ~FPSStatus.TEMPERATURE_NORMAL) | FPSStatus.TEMPERATURE_UNKNOWN) # Issue an update status to get the status set. await self.update_status() # Start the pollers if start_pollers and not self.is_bootloader(): self.pollers.start() # Initialise alerts and chiller bots with a bit of delay to let the actor # time to start. asyncio.create_task(self.alerts.start(delay=5)) asyncio.create_task(self.chiller.start(delay=5)) return self
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 send_trajectory( fps: FPS, trajectories: str | pathlib.Path | TrajectoryDataType, use_sync_line: bool | None = None, send_trajectory: bool = True, start_trajectory: bool = True, command: Optional[CluCommand[JaegerActor]] = None, dump: bool | str = True, extra_dump_data: dict[str, Any] = {}, ) -> Trajectory: """Sends a set of trajectories to the positioners. This is a low-level function that raises errors when a problem is encountered. Most users should use `.FPS.send_trajectory` instead. `.send_trajectory` automates `.Trajectory` by calling the different methods in order, but provides less control. Parameters ---------- fps The instance of `.FPS` that will receive the trajectory. trajectories Either a path to a YAML file to read or a dictionary with the trajectories. In either case the format must be a dictionary in which the keys are the ``positioner_ids`` and each value is a dictionary containing two keys: ``alpha`` and ``beta``, each pointing to a list of tuples ``(position, time)``, where ``position`` is in degrees and ``time`` is in seconds. use_sync_line If `True`, the SYNC line will be used to synchronise the beginning of all trajectories. Otherwise a `.START_TRAJECTORY` command will be sent over the CAN network. If `None`, defaults to the configuration parameter. send_trajectory If `True`, sends the trajectory to the positioners and returns the `.Trajectory` instance. start_trajectory If `True`, runs the trajectory after sending it. Otherwise, returns the `.Trajectory` instance after sending the data. Ignored if `send_trajectory=False`. command A command to which to output informational messages. dump Whether to dump a JSON with the trajectory to disk. If `True`, the trajectory is stored in ``/data/logs/jaeger/trajectories/<MJD>`` with a unique sequence for each trajectory. A string can be passed with a custom path. The dump file is created only if `.Trajectory.start` is called, regardless of whether the trajectory succeeds. extra_dump_data A dictionary with additional parameters to add to the dump JSON. Raises ------ TrajectoryError If encounters a problem sending the trajectory. FPSLockedError If the FPS is locked. Examples -------- :: >>> fps = FPS() >>> await fps.initialise() # Send a trajectory with two points for positioner 4 >>> await fps.send_trajectory({4: {'alpha': [(90, 0), (91, 3)], 'beta': [(20, 0), (23, 4)]}}) """ traj = Trajectory( fps, trajectories, dump=dump, extra_dump_data=extra_dump_data, ) if use_sync_line is None: use_sync_line = config["fps"]["use_sync_line"] if use_sync_line: if not isinstance(fps.ieb, IEB) or fps.ieb.disabled: raise TrajectoryError( "IEB is not connected. Cannot use SYNC line.", traj) if (await fps.ieb.get_device("sync").read())[0] == "closed": raise TrajectoryError("The SYNC line is on high.", traj) if send_trajectory is False: return traj msg = "Sending trajectory data." log.debug(msg) if command: command.debug(msg) try: await traj.send() except TrajectoryError as err: raise TrajectoryError( f"Something went wrong sending the trajectory: {err}", err.trajectory, ) finally: if traj.dump_file: traj.dump_trajectory() if command: command.debug(trajectory_dump_file=traj.dump_file) msg = f"Trajectory sent in {traj.data_send_time:.1f} seconds." log.debug(msg) if command: command.debug(msg) log.info( f"Expected time to complete trajectory: {traj.move_time:.2f} seconds.") if command and traj.move_time: command.info(move_time=round(traj.move_time, 2)) if start_trajectory is False: return traj msg = "Starting trajectory ..." log.info(msg) if command: command.info(msg) try: await traj.start(use_sync_line=use_sync_line) except TrajectoryError as err: if traj.start_time is not None and traj.end_time is not None: elapsed = traj.end_time - traj.start_time elapsed_msg = f" Trajectory failed {elapsed:.2f} seconds after start." else: elapsed_msg = "" raise TrajectoryError( f"Something went wrong during the trajectory: {err}.{elapsed_msg}", err.trajectory, ) finally: if traj.dump_file and command: command.debug(trajectory_dump_file=traj.dump_file) if command: command.info(folded=(await fps.is_folded())) msg = "All positioners have reached their destinations." log.info(msg) if command: command.info(msg) return traj
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)