Esempio n. 1
0
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
Esempio n. 2
0
File: fps.py Progetto: sdss/jaeger
    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__]
Esempio n. 3
0
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
Esempio n. 4
0
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,
    )
Esempio n. 5
0
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)}.")
Esempio n. 6
0
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
Esempio n. 7
0
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)
Esempio n. 8
0
File: fps.py Progetto: sdss/jaeger
    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
Esempio n. 9
0
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
Esempio n. 10
0
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
Esempio n. 11
0
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)