示例#1
0
文件: __main__.py 项目: sdss/jaeger
async def status(fps_maker: FPSWrapper, positioner_id: int):
    """Returns the status of a positioner with low-level initialisation."""

    fps_maker.initialise = False

    async with fps_maker as fps:

        pos = Positioner(positioner_id, fps)

        try:
            await pos.update_firmware_version()
            print(f"Firmware: {pos.firmware}")
            print(f"Bootloader: {pos.is_bootloader()}")
        except Exception as err:
            raise JaegerError(f"Failed retrieving firmware: {err}")

        try:
            await pos.update_status()
            bit_names = ", ".join(bit.name for bit in pos.status.active_bits)
            print(f"Status: {pos.status.value} ({bit_names})")
        except Exception as err:
            raise JaegerError(f"Failed retrieving status: {err}")

        try:
            await pos.update_position()
            print(f"Position: {pos.position}")
        except Exception as err:
            raise JaegerError(f"Failed retrieving position: {err}")
示例#2
0
def get_sjd(observatory: Optional[str] = None) -> int:
    """Returns the SDSS Julian Date as an integer based on the observatory.

    Parameters
    ----------
    observatory
        The current observatory, either APO or LCO. If `None`, uses ``$OBSERVATORY``.

    """

    if observatory is None:
        try:
            observatory = os.environ["OBSERVATORY"]
        except KeyError:
            raise JaegerError(
                "Observatory not passed and $OBSERVATORY is not set.")

    observatory = observatory.upper()
    if observatory not in ["APO", "LCO"]:
        raise JaegerError(f"Invalid observatory {observatory}.")

    time = Time.now()
    mjd = time.mjd

    if observatory == "APO":
        return int(mjd + 0.3)
    else:
        return int(mjd + 0.7)
示例#3
0
文件: actor.py 项目: sdss/jaeger
    def __init__(
        self,
        fps: FPS,
        *args,
        ieb_status_delay: float = 60.0,
        observatory: str | None = None,
        **kwargs,
    ):

        jaeger.actor_instance = self

        self.fps = fps

        # This is mostly for the miniwoks. If the schema file is not the base
        # one, merge them.
        base = os.path.join(os.path.dirname(__file__), "..")

        base_schema = os.path.realpath(os.path.join(base, "etc/schema.json"))

        schema = kwargs.get("schema", None)
        c_schema = os.path.realpath(os.path.join(base,
                                                 schema)) if schema else None

        kwargs["schema"] = merge_json(base_schema,
                                      c_schema,
                                      write_temporary_file=True)

        # Pass the FPS instance as the second argument to each parser
        # command (the first argument is always the actor command).
        self.parser_args = [fps]

        # Set the observatory where the actor is running.
        if observatory is None:
            try:
                self.observatory = os.environ["OBSERVATORY"]
            except KeyError:
                raise JaegerError(
                    "Observatory not passed and $OBSERVATORY is not set.")
        else:
            self.observatory = observatory

        super().__init__(*args, **kwargs)

        self.version = __version__

        # Add ActorHandler to log and to the warnings logger.
        self.actor_handler = ActorHandler(
            self,
            level=logging.WARNING,
            filter_warnings=[JaegerUserWarning],
        )
        log.addHandler(self.actor_handler)
        if log.warnings_logger:
            log.warnings_logger.addHandler(self.actor_handler)

        self._alive_task = asyncio.create_task(self._report_alive())
        self._status_watcher_task = asyncio.create_task(self._status_watcher())

        self.fps.alerts.set_actor(self)
        self.fps.chiller.set_actor(self)
示例#4
0
文件: positioner.py 项目: sdss/jaeger
    async def wait_for_status(
        self,
        status: List[maskbits.PositionerStatus],
        delay=1,
        timeout: Optional[float] = None,
    ) -> bool:
        """Polls the status until it reaches a certain value.

        Parameters
        ----------
        status
            The status to wait for. Can be a list in which case it will wait
            until all the statuses in the list have been reached.
        delay
            Time, in seconds, to wait between position updates.
        timeout
            How many seconds to wait for the status to reach the desired value
            before aborting.

        Returns
        -------
        result
            Returns `True` if the status has been reached or `False` if the
            timeout limit was reached.

        """

        if self.is_bootloader():
            raise JaegerError(
                "wait_for_status cannot be scheduled in bootloader mode.")

        if not isinstance(status, (list, tuple)):
            status = [status]

        async def status_waiter(wait_for_status):

            while True:
                await self.update_status()
                # Check all statuses in the list
                all_reached = True
                for ss in wait_for_status:
                    if ss not in self.status:
                        all_reached = False
                        break

                if all_reached:
                    return

                await asyncio.sleep(delay)

        wait_for_status = [self.flags(int(ss)) for ss in status]

        try:
            await asyncio.wait_for(status_waiter(wait_for_status), timeout)
        except asyncio.TimeoutError:
            return False

        return True
示例#5
0
文件: __main__.py 项目: sdss/jaeger
async def goto(
    fps_maker,
    positioner_ids: tuple[int, ...] | list[int],
    alpha: float,
    beta: float,
    speed: float | None,
    all: bool = False,
    force: bool = False,
    relative: bool = False,
    use_sync: bool = True,
    go_cowboy: bool = False,
):
    """Sends positioners to a given (alpha, beta) position."""

    with fps_maker as fps:

        if all:
            if not force:
                raise JaegerError(
                    "Use --force to move all positioners at once.")
            positioner_ids = list(fps.positioners.keys())
        else:
            positioner_ids = list(positioner_ids)

        if not relative:
            if alpha < 0 or beta < 0:
                raise JaegerError(
                    "Negative angles only allowed in relative mode.")

        new_positions = {}
        for pid in positioner_ids:
            new_positions[pid] = (alpha, beta)

        try:
            await goto_(
                fps,
                new_positions,
                speed=speed,
                relative=relative,
                use_sync_line=use_sync,
                go_cowboy=go_cowboy,
            )
        except (JaegerError, TrajectoryError) as err:
            raise JaegerError(f"Goto command failed: {err}")
示例#6
0
文件: fps.py 项目: sdss/jaeger
    async def send_to_all(self, *args, **kwargs):
        """Sends a command to all connected positioners.

        This method has been deprecated. Use `.send_command` with a list
        for ``positioner_ids`` instead.

        """

        raise JaegerError(
            "send_to_all has been deprecated. Use send_command instead.")
示例#7
0
文件: kaiju.py 项目: sdss/jaeger
def get_snapshot_async(
    path: str,
    robot_grid: Optional[RobotGridCalib] = None,
    data: Optional[dict] = None,
    highlight: int | None = None,
    title: str | None = None,
):
    """Creates an FPS snapshot and saves it to disk. To be used with an executor.

    Parameters
    ----------
    path
        The path where to save the file.
    robot_grid
        The Kaiju ``RobotGridCalib`` instance to plot.
    data
        A dictionary of data that can be used to reload a Kaiju robot grid
        using `.load_robot_grid`. This is useful if the function is being
        run in an executor.
    highlight
        Robot to highlight.
    title
        A title for the plot.

    """

    import matplotlib.pyplot as plt

    if robot_grid is not None and data is not None:
        raise JaegerError("robot_grid and data are mutually exclusive.")

    if data is not None:
        robot_grid = load_robot_grid(data)

    assert robot_grid is not None

    with warnings.catch_warnings():
        warnings.filterwarnings("ignore",
                                message=".+array interface is deprecated.+")
        ax = robot_grid.plot_state(highlightRobot=highlight)

    assert ax is not None

    if title is not None:
        ax.set_title(title)

    plt.tight_layout()

    ax.figure.savefig(path)

    plt.close("all")
示例#8
0
文件: kaiju.py 项目: sdss/jaeger
def get_robot_grid(fps: FPS | None,
                   seed: int | None = None,
                   collision_buffer=None):
    """Returns a new robot grid with the destination set to the lattice position.

    If an initialised instance of the FPS is available, disabled robots will be
    set offline in the grid at their current positions.

    """

    from kaiju.robotGrid import RobotGridCalib

    if seed is None:
        t = 1000 * time.time()
        seed = int(int(t) % 2**32 / 1000)

    kaiju_config = config["kaiju"]
    ang_step = kaiju_config["ang_step"]
    collision_buffer = collision_buffer or kaiju_config["collision_buffer"]
    alpha0, beta0 = kaiju_config["lattice_position"]
    epsilon = ang_step * kaiju_config["epsilon_factor"]

    if collision_buffer < 1.5:
        raise JaegerError("Invalid collision buffer < 1.5.")

    log.debug(
        f"Creating RobotGridCalib with stepSize={ang_step}, epsilon={epsilon}."
    )

    robot_grid = RobotGridCalib(stepSize=ang_step, epsilon=epsilon, seed=seed)
    robot_grid.setCollisionBuffer(collision_buffer)

    # TODO: This is a bit hacky. Kaiju doesn't have a collisionBuffer anymore
    # as collision buffers are per robot, but I want to keep this information
    # for when I dump and reload robot grids.
    robot_grid.collisionBuffer = collision_buffer

    for robot in robot_grid.robotDict.values():
        if fps is not None and robot.id in fps and fps[
                robot.id].disabled is True:
            positioner = fps[robot.id]
            robot.setDestinationAlphaBeta(positioner.alpha, positioner.beta)
            robot.setAlphaBeta(positioner.alpha, positioner.beta)
            robot.isOffline = True
        else:
            robot.setDestinationAlphaBeta(alpha0, beta0)
            robot.setAlphaBeta(alpha0, beta0)

    return robot_grid
示例#9
0
文件: fps.py 项目: sdss/jaeger
    async def unlock(self, force=False):
        """Unlocks the `.FPS` if all collisions have been resolved."""

        # Send STOP_TRAJECTORY. This clears the collided flags.
        await self.stop_trajectory(clear_flags=True)

        await self.update_status(timeout=2)

        for positioner in self.positioners.values():
            if positioner.collision:
                self._locked = True
                raise JaegerError("Cannot unlock the FPS until all the "
                                  "collisions have been cleared.")

        self._locked = False
        self.locked_by = []

        return True
示例#10
0
文件: fps.py 项目: sdss/jaeger
    def __new__(cls: Type[FPS_T], *args, **kwargs):

        if cls in cls._instance and kwargs == {}:
            raise JaegerError("An instance of FPS is already running. "
                              "Use get_instance() to retrieve it.")

        if cls not in cls._instance or kwargs != {}:

            if cls not in cls._instance:
                new_obj = super().__new__(cls)
                cls._instance[cls] = new_obj
            else:
                new_obj = cls._instance[cls]

            dict.__init__(new_obj, {})
            new_obj.initialised = False

            return new_obj

        raise RuntimeError(
            "Returning None in BaseFPS.__new__. This should not happen.")
示例#11
0
文件: fps.py 项目: sdss/jaeger
    async def start_can(self):
        """Starts the JaegerCAN interface."""

        use_lock = config["fps"]["use_lock"]

        if use_lock and self.pid_lock is None:
            try:
                if not os.path.exists(os.path.dirname(LOCK_FILE)):
                    os.makedirs(os.path.dirname(LOCK_FILE))
                self.pid_lock = LockFile(LOCK_FILE)
            except Exception:
                raise JaegerError(
                    f"Failed creating lock file {LOCK_FILE}. "
                    "Probably another instance is running. "
                    "If that is not the case, remove the lock file and retry.")

        if isinstance(self.can, JaegerCAN):
            await self.can.start()
            return

        self.can = await JaegerCAN.create(self.can, fps=self)
        return True
示例#12
0
文件: fps.py 项目: sdss/jaeger
    def add_positioner(
            self,
            positioner: int | Positioner,
            centre=(None, None),
    ) -> Positioner:
        """Adds a new positioner to the list, and checks for duplicates."""

        if isinstance(positioner, self.positioner_class):
            positioner_id = positioner.positioner_id
        else:
            positioner_id = positioner
            positioner = self.positioner_class(positioner_id,
                                               None,
                                               centre=centre)

        if positioner_id in self.positioners:
            raise JaegerError(f"{self.__class__.__name__}: there is already a "
                              f"positioner in the list with positioner_id "
                              f"{positioner_id}.")

        self.positioners[positioner_id] = positioner

        return positioner
示例#13
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)
示例#14
0
文件: bootloader.py 项目: sdss/jaeger
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
示例#15
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
示例#16
0
async def goto(
    fps: FPS,
    new_positions: dict[int, tuple[float, float]],
    speed: Optional[float] = None,
    relative: bool = False,
    use_sync_line: bool = None,
    go_cowboy: bool = False,
    force: bool = False,
    command: CluCommand[JaegerActor] | None = None,
):
    """Send positioners to a given position using a trajectory with ``kaiju`` check.

    Parameters
    ----------
    fps
        The `.FPS` instance.
    new_positions
        The new positions as a dictionary of positioner ID to a tuple of new
        alpha and beta angles. Positioners not specified will be kept on the
        same positions.
    speed
        The speed to use.
    relative
        If `True`, ``alpha`` and ``beta`` are considered relative angles.
    use_sync_line
        Whether to use the SYNC line to start the trajectories.
    go_cowboy
        If set, does not create a ``kaiju``-safe trajectory. Use at your own risk.
    force
        If ``go_cowboy=False`` and the trajectory is deadlocked, a `.TrajectoryError`
        will be raised. Use ``force=True`` to apply the trajectory anyway.
    command
        A command to pass to `.send_trajectory` to output additional information.

    """

    if fps.locked:
        FPSLockedError("The FPS is locked.")

    if fps.moving:
        raise JaegerError("FPS is moving. Cannot send goto.")

    for pid in new_positions:
        if pid not in fps.positioners:
            raise JaegerError(f"Positioner ID {pid} is not connected.")

    speed = float(speed or config["positioner"]["motor_speed"])
    if speed < 500 or speed > 5000:
        raise JaegerError("Invalid speed.")

    positioner_ids = list(new_positions.keys())
    await fps.update_position(positioner_ids=positioner_ids)

    trajectories = {}

    if go_cowboy is True:
        for pid in positioner_ids:
            pos = fps[pid]

            if pos.alpha is None or pos.beta is None:
                raise JaegerError(
                    f"Positioner {pid}: cannot goto with unknown position.")

            current_alpha = pos.alpha
            current_beta = pos.beta

            if relative is True:
                alpha_end = current_alpha + new_positions[pid][0]
                beta_end = current_beta + new_positions[pid][1]
            else:
                alpha_end = new_positions[pid][0]
                beta_end = new_positions[pid][1]

            alpha_delta = abs(alpha_end - current_alpha)
            beta_delta = abs(beta_end - current_beta)

            time_end = [
                get_goto_move_time(alpha_delta, speed=speed),
                get_goto_move_time(beta_delta, speed=speed),
            ]

            trajectories[pid] = {
                "alpha": [(current_alpha, 0.1),
                          (alpha_end, time_end[0] + 0.1)],
                "beta": [(current_beta, 0.1), (beta_end, time_end[1] + 0.1)],
            }

    else:
        if relative is True:
            raise JaegerError("relative is not implemented for kaiju moves.")

        data = {"collision_buffer": None, "grid": {}}

        for pid, (current_alpha,
                  current_beta) in fps.get_positions_dict().items():

            if current_alpha is None or current_beta is None:
                raise JaegerError(
                    f"Positioner {pid} does not know its position.")

            if pid in new_positions:
                data["grid"][int(pid)] = (
                    current_alpha,
                    current_beta,
                    new_positions[pid][0],
                    new_positions[pid][1],
                    fps[pid].disabled,
                )
            else:
                data["grid"][int(pid)] = (
                    current_alpha,
                    current_beta,
                    current_alpha,
                    current_beta,
                    fps[pid].disabled,
                )

        (to_destination, _, did_fail, deadlocks) = await run_in_executor(
            get_path_pair,
            data=data,
            stop_if_deadlock=force,
            executor="process",
            ignore_did_fail=force,
        )

        if did_fail is True:
            if force is False:
                raise TrajectoryError(
                    "Cannot execute trajectory. Found "
                    f"{len(deadlocks)} deadlocks ({deadlocks}).")
            else:
                warnings.warn(
                    f"Found {len(deadlocks)} deadlocks but applying trajectory.",
                    JaegerUserWarning,
                )

        trajectories = to_destination

    return await send_trajectory(
        fps,
        trajectories,
        use_sync_line=use_sync_line,
        command=command,
        extra_dump_data={"kaiju_trajectory": not go_cowboy},
    )
示例#17
0
文件: fps.py 项目: sdss/jaeger
    def send_command(
        self,
        command: str | int | CommandID | Command,
        positioner_ids: int | List[int] | None = None,
        data: Any = None,
        now: bool = False,
        **kwargs,
    ) -> Command:
        """Sends a command to the bus.

        Parameters
        ----------
        command
            The ID of the command, either as the integer value, a string,
            or the `.CommandID` flag. Alternatively, the `.Command` to send.
        positioner_ids
            The positioner IDs to command, or zero for broadcast. If `None`,
            sends the command to all FPS non-disabled positioners.
        data
            The bytes to send. See `.Command` for details on the format.
        interface
            The index in the interface list for the interface to use. Only
            relevant in case of a multibus interface. If `None`, the positioner
            to bus map will be used.
        bus
            The bus within the interface to be used. Only relevant in case of
            a multibus interface. If `None`, the positioner to bus map will
            be used.
        now
            If `True`, the command is sent to the CAN network immediately,
            skipping the command queue. No tracking is done for this command.
            It should only be used for emergency and shutdown commands.
        kwargs
            Extra arguments to be passed to the command.

        Returns
        -------
        command
            The command sent to the bus. The command needs to be awaited
            before it is considered done.

        """

        if not isinstance(self.can, JaegerCAN) or self.can._started is False:
            raise JaegerError("CAN connection not established.")

        if positioner_ids is None:
            positioner_ids = [p for p in self if not self[p].disabled]

        if not isinstance(command, Command):
            command_flag = CommandID(command)
            CommandClass = command_flag.get_command_class()
            assert CommandClass, "CommandClass not defined"

            command = CommandClass(positioner_ids, data=data, **kwargs)

        assert isinstance(command, Command)

        broadcast = command.is_broadcast
        pids = command.positioner_ids

        if broadcast:
            if any([self[pos].disabled for pos in self]) and not command.safe:
                raise JaegerError("Some positioners are disabled.")
        else:
            if any([self[pid].disabled for pid in pids]) and not command.safe:
                raise JaegerError("Some commanded positioners are disabled.")

        if not broadcast and not all([p in self for p in pids]):
            raise JaegerError("Some positioners are not connected.")

        # Check if we are in bootloader mode.
        in_boot = [p for p in pids if p != 0 and self[p].is_bootloader()]
        if (broadcast and self.is_bootloader()) or (not broadcast
                                                    and any(in_boot)):
            if not command.bootloader:
                raise JaegerError(
                    f"Cannot send command {command.command_id.name!r} "
                    "while in bootloader mode.")

        command_name = command.name
        command_uid = command.command_uid
        header = f"({command_name}, {command_uid}): "

        if self.locked:
            if command.safe:
                log.debug(f"FPS is locked but {command_name} is safe.")
            else:
                command.cancel(silent=True)
                raise FPSLockedError("FPS is locked.")

        elif command.move_command and self.moving:
            command.cancel(silent=True)
            raise JaegerError("Cannot send move command while the "
                              "FPS is moving. Use FPS.stop_trajectory() "
                              "to stop the FPS.")

        if command.status.is_done:
            raise JaegerError(header + "trying to send a done command.")

        if not now:
            assert self.can.command_queue
            self.can.command_queue.put_nowait(command)
            can_log.debug(header + "added command to CAN processing queue.")
        else:
            self.can.send_messages(command)
            can_log.debug(header + "sent command to CAN immediately.")

        return command
示例#18
0
文件: fps.py 项目: 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
示例#19
0
文件: base.py 项目: sdss/jaeger
    def __init__(
        self,
        positioner_ids: int | List[int],
        timeout: Optional[float] = None,
        done_callback: Optional[Callable] = None,
        n_positioners: Optional[int] = None,
        data: Union[None, data_co, Dict[int, data_co]] = None,
        ignore_unknown: bool = True,
    ):

        global COMMAND_UID

        assert self.broadcastable is not None, "broadcastable not set"
        assert self.command_id is not None, "command_id not set"

        if isinstance(positioner_ids, (list, tuple)):
            self.positioner_ids = list(positioner_ids)
            if len(positioner_ids) != len(set(positioner_ids)):
                raise JaegerError("The list of positioner_ids must be unique.")
            if len(positioner_ids) > 1 and 0 in positioner_ids:
                raise JaegerError(
                    "Broadcasts cannot be mixed with other positioners.")
        else:
            self.positioner_ids = [positioner_ids]

        if self.is_broadcast and self.broadcastable is False:
            raise JaegerError(
                f"Command {self.command_id.name} cannot be broadcast.")

        #: The data payload for the messages to send.
        if data is None:
            self.data = {pid: [bytearray()] for pid in self.positioner_ids}
        elif isinstance(data, bytearray):
            self.data = {pid: [data] for pid in self.positioner_ids}
        elif isinstance(data, (list, tuple)):
            self.data = {pid: data for pid in self.positioner_ids}
        elif isinstance(data, dict):
            self.data = {}
            for pid, value in data.items():
                if value is None:
                    self.data[pid] = [bytearray()]
                elif isinstance(value, (list, tuple)):
                    self.data[pid] = value
                elif isinstance(value, bytearray):
                    self.data[pid] = [value]
                else:
                    raise ValueError(f"Invalid data {value!r}.")
        else:
            raise ValueError(f"Invalid data {data!r}.")

        if self.is_broadcast:
            if len(self.data[0]) > 1:
                raise CommandError(
                    "Broadcasts can only include a single data packet.")

        # Number of replies expected unless broadcast.
        if self.is_broadcast:
            if n_positioners:
                self._n_replies = len(self.data[0]) * n_positioners
            else:
                self._n_replies = None
        else:
            self._n_replies = sum([len(value) for value in self.data.values()])

        if timeout is None:
            pass
        else:
            self.timeout = timeout
            if self.timeout < 0 and self._n_replies is None:
                raise CommandError(
                    "In a broadcast a timeout is required unless n_positioners is set."
                )

        #: A list of messages with the responses to this command.
        self.replies: List[Reply] = []

        # Messages sent.
        self.messages = []
        self.message_uids = []

        # Generate a UUID for this command.
        self.command_uid = COMMAND_UID
        COMMAND_UID += 1

        # Starting and end time
        self.start_time: float | None = None
        self.end_time: float | None = None

        # If this is the first time we run this command the pool will be empty.
        uid_bits = config["positioner"]["uid_bits"]
        command_pool = UID_POOL[self.command_id]
        if not self.is_broadcast:
            for pid in self.positioner_ids:
                if pid not in command_pool:
                    command_pool[pid] = set(range(1, 2**uid_bits))
        else:
            if 0 not in command_pool:
                command_pool[0] = set([0])

        # What interface and bus this command should be sent to. Only relevant
        # for multibus interfaces. To be filled by the FPS class when queueing
        # the command.
        self._interfaces: Optional[List[BusABC]] = []
        self._bus: Optional[int] = None

        self._done_callback = done_callback

        self._timeout_handle = None

        self._ignore_unknown = ignore_unknown
        self.loop = asyncio.get_event_loop()

        StatusMixIn.__init__(
            self,
            maskbit_flags=CommandStatus,
            initial_status=CommandStatus.READY,
            callback_func=self.status_callback,
        )

        asyncio.Future.__init__(self)
示例#20
0
文件: kaiju.py 项目: sdss/jaeger
def get_path_pair(
    robot_grid: Optional[RobotGridCalib] = None,
    data: Optional[dict] = None,
    path_generation_mode: str = "greedy",
    ignore_did_fail: bool = False,
    explode_deg: float = 5,
    explode_positioner_id: int | None = None,
    speed=None,
    smooth_points=None,
    path_delay=None,
    collision_shrink=None,
    stop_if_deadlock: bool = False,
    ignore_initial_collisions: bool = False,
) -> tuple:
    """Runs ``pathGenGreedy`` and returns the to and from destination paths.

    Parameters
    ----------
    robot_grid
        The Kaiju ``RobotGridCalib`` instance to decollide.
    data
        A dictionary of data that can be used to reload a Kaiju robot grid
        using `.load_robot_grid`. This is useful if the function is being
        run in an executor.
    path_generation_mode
        Defines the path generation algorithm to use.
        Either ``greedy`` or ``explode`` or ``explode_one``.
    ignore_did_fail
        Generate paths even if path generation failed (i.e., deadlocks).
    explode_deg
        Degrees for ``pathGenExplode``.
    explode_positioner_id
        The positioner to explode.
    speed, smooth_points, path_delay, collision_shrink
        Kaiju parameters to pass to ``getPathPair``. Otherwise uses the default
        configuration values.
    stop_if_deadlock
        If `True`, detects deadlocks early in the path and returns shorter
        trajectories (at the risk of some false positive deadlocks).
    ignore_initial_collisions
        If `True`, does not fail if the initial state is collided. To be used
        only for offsets.

    Returns
    -------
    paths
        A tuple with the to destination path, from destination path, whether path
        generation failed, and the list of deadlocks

    """

    if robot_grid is not None and data is not None:
        raise JaegerError("robot_grid and data are mutually exclusive.")

    if data is not None:
        set_destination = False if path_generation_mode == "explode" else True
        robot_grid = load_robot_grid(data, set_destination=set_destination)

    assert robot_grid is not None

    if path_generation_mode == "explode":
        log.debug(f"Running pathGenExplode with explode_deg={explode_deg}.")
        robot_grid.pathGenExplode(explode_deg)
        deadlocks = []
    elif path_generation_mode == "explode_one":
        log.debug(f"Running pathGenExplodeOne with explode_deg={explode_deg}, "
                  f"explode_positioner_id={explode_positioner_id}.")
        robot_grid.pathGenExplodeOne(explode_deg, explode_positioner_id)
        deadlocks = []
    else:
        log.debug(
            f"Running pathGenGreedy with stopIfDeadlock={stop_if_deadlock}.")
        robot_grid.pathGenGreedy(
            stopIfDeadlock=stop_if_deadlock,
            ignoreInitialCollisions=ignore_initial_collisions,
        )

        # Check for deadlocks.
        deadlocks = robot_grid.deadlockedRobots()
        if robot_grid.didFail and ignore_did_fail is False:
            return (None, None, robot_grid.didFail, deadlocks)

    speed = speed or config["kaiju"]["speed"]
    smooth_points = smooth_points or config["kaiju"]["smooth_points"]
    collision_shrink = collision_shrink or config["kaiju"]["collision_shrink"]
    path_delay = path_delay or config["kaiju"]["path_delay"]

    log.debug(
        f"Running getPathPair with speed={speed}, smoothPoints={smooth_points}, "
        f"collisionShrink={collision_shrink}, pathDelay={path_delay}.")

    to_destination, from_destination = robot_grid.getPathPair(
        speed=speed,
        smoothPoints=smooth_points,
        collisionShrink=collision_shrink,
        pathDelay=path_delay,
    )

    return (
        to_destination,
        from_destination,
        robot_grid.didFail,
        deadlocks,
    )
示例#21
0
文件: kaiju.py 项目: sdss/jaeger
def decollide(
    robot_grid: Optional[RobotGridCalib] = None,
    data: Optional[dict] = None,
    simple: bool = False,
    decollide_grid_fallback: bool = False,
    priority_order: list[int] = [],
) -> tuple[RobotGridCalib | dict, list[int]]:
    """Decollides a potentially collided grid. Raises on fail.

    Parameters
    ----------
    robot_grid
        The Kaiju ``RobotGridCalib`` instance to decollide.
    data
        A dictionary of data that can be used to reload a Kaiju robot grid
        using `.load_robot_grid`. This is useful if the function is being
        run in an executor.
    simple
        Runs ``decollideGrid()`` and returns.
    decollide_grid_fallback
        If `True`, runs ``decollideGrid()`` if the positioner-by-positioner
        decollision fails.
    priority_list
        A sorted list of positioner IDs with the order of which positioners to
        try to keep at their current positions. Positioners earlier in the list
        will be decollided last. Ignore in case of ``simple=True``.

    Returns
    -------
    grid,decollided
        If ``robot_grid`` is passed, returns the same grid instance after decollision.
        If ``data`` is passed, returns a dictionary describing the decollided grid
        that can be used to recreate a grid using `.load_robot_grid`. Also returns
        the list of robots that have been decollided.

    """

    if robot_grid is not None and data is not None:
        raise JaegerError("robot_grid and data are mutually exclusive.")

    if data is not None:
        robot_grid = load_robot_grid(data)

    assert robot_grid is not None

    if simple:
        collided = robot_grid.getCollidedRobotList()
        robot_grid.decollideGrid()
        if len(robot_grid.getCollidedRobotList()) > 0:
            raise JaegerError("Failed decolliding grid.")

        if data is not None:
            return dump_robot_grid(robot_grid), collided
        else:
            return robot_grid, collided

    # First pass. If collided, decollide each robot one by one.
    collided = robot_grid.getCollidedRobotList()

    # Sort collided robots by priority order.
    collided = sorted(
        collided,
        key=lambda x: len(priority_order) - priority_order.index(x)
        if x in priority_order else -1,
    )

    decollided = []
    if len(collided) > 0:
        for robot_id in collided:
            if robot_grid.isCollided(robot_id):
                if robot_grid.robotDict[robot_id].isOffline:
                    continue
                robot_grid.decollideRobot(robot_id)
                decollided.append(
                    robot_id)  # Even if we failed it may have moved.
                if robot_grid.isCollided(robot_id):
                    raise JaegerError(
                        f"Failed decolliding positioner {robot_id}.")

    # Second pass. If still collided, try a grid decollision.
    if len(robot_grid.getCollidedRobotList()) > 0:
        if decollide_grid_fallback:
            warn("Grid is still colliding. Attempting full grid decollision.")
            robot_grid.decollideGrid()
            if robot_grid.getCollidedRobotList() is not False:
                raise JaegerError("Failed decolliding grid.")
            # We don't know which robots were decollided so assume all collided
            # robots have moved.
            decollided = collided
        else:
            raise JaegerError("Failed decolliding grid.")

    if data is not None:
        return dump_robot_grid(robot_grid), decollided
    else:
        return robot_grid, decollided