示例#1
0
    async def abort_trajectory(self):
        """Aborts the trajectory transmission."""

        cmd = await self.fps.send_command(
            "SEND_TRAJECTORY_ABORT",
            positioner_ids=list(self.trajectories.keys()),
        )

        if cmd.status.failed:
            raise TrajectoryError("Cannot abort trajectory transmission.",
                                  self)
示例#2
0
    def validate(self):
        """Validates the trajectory."""

        if len(self.trajectories) == 0:
            raise TrajectoryError("trajectory is empty.", self)

        if len(self.trajectories) != len(numpy.unique(list(
                self.trajectories))):
            raise TrajectoryError("Duplicate positioner trajectories.", self)

        for pid in self.trajectories:
            trajectory = self.trajectories[pid]

            if "alpha" not in trajectory or "beta" not in trajectory:
                self.failed_positioners[pid] = "NO_DATA"
                raise TrajectoryError(
                    f"Positioner {pid} missing alpha or beta data.",
                    self,
                )

            for arm in ["alpha", "beta"]:
                data = numpy.array(list(zip(*trajectory[arm]))[0])

                if arm == "beta":
                    if config.get("safe_mode", False):
                        if config["safe_mode"] is True:
                            min_beta = 160
                        else:
                            min_beta = config["safe_mode"]["min_beta"]
                        if numpy.any(data < min_beta):
                            self.failed_positioners[pid] = "SAFE_MODE"
                            raise TrajectoryError(
                                f"Positioner {pid}: safe mode is on "
                                f"and beta < {min_beta}.",
                                self,
                            )
示例#3
0
文件: kaiju.py 项目: sdss/jaeger
async def unwind(
    current_positions: dict[int, tuple[float | None, float | None]],
    collision_buffer: float | None = None,
    disabled: list[int] = [],
    force: bool = False,
):
    """Folds all the robots to the lattice position.

    This coroutine uses a process pool executor to run Kaiju routines.

    """

    alpha0, beta0 = config["kaiju"]["lattice_position"]

    # We create the data directly since it's simple. This should be a bit faster
    # than creating a grid and dumping it.
    data = {"collision_buffer": collision_buffer, "grid": {}}
    for pid, (alpha, beta) in current_positions.items():
        data["grid"][int(pid)] = (alpha, beta, alpha0, beta0, pid in disabled)

    (to_destination, _, did_fail, deadlocks) = await run_in_executor(
        get_path_pair,
        data=data,
        ignore_did_fail=force,
        stop_if_deadlock=force,
        executor="process",
    )
    if did_fail:
        if force is False:
            raise TrajectoryError(
                "Failed generating a valid unwind trajectory. "
                f"{len(deadlocks)} deadlocks were found ({deadlocks}).")
        else:
            log.warning("Deadlocks found in unwind but proceeding anyway.")

    return to_destination
示例#4
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},
    )
示例#5
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
示例#6
0
    async def start(self, use_sync_line=True):
        """Starts the trajectory."""

        if not self._ready_to_start or self.failed:
            raise TrajectoryError("The trajectory has not been sent.", self)

        if self.move_time is None:
            raise TrajectoryError("move_time not set.", self)

        self.use_sync_line = use_sync_line

        if use_sync_line:

            if not isinstance(self.fps.ieb, IEB) or self.fps.ieb.disabled:
                raise TrajectoryError(
                    "IEB is not connected. Cannot use SYNC line.",
                    self,
                )
            if (await self.fps.ieb.get_device("sync").read())[0] == "closed":
                raise TrajectoryError("The SYNC line is on high.", self)

            sync = self.fps.ieb.get_device("sync")
            assert isinstance(sync, drift.Relay)

            # Set SYNC line to high.
            await sync.close()

            # Schedule reseting of SYNC line
            asyncio.get_event_loop().call_later(0.5, asyncio.create_task,
                                                sync.open())

        else:

            # Start trajectories
            command = await self.fps.send_command(
                "START_TRAJECTORY",
                positioner_ids=0,
                timeout=1,
                # All positioners reply, including those not in the trajectory.
                n_positioners=len(self.fps.positioners),
            )

            if command.status.failed:
                await self.fps.stop_trajectory()
                self.failed = True
                raise TrajectoryError("START_TRAJECTORY failed", self)

        restart_pollers = True if self.fps.pollers.running else False
        await self.fps.pollers.stop()

        self.start_time = time.time()

        # min_trajectory_time = 5.0
        # PS = PositionerStatus

        try:

            # # The positioners take a bit to report that they are moving so if the
            # # move time is too short, we don't try to check if the positioners started
            # # moving (but we'll check later that they arrived to their positions.)
            # if self.move_time >= min_trajectory_time:
            #     await asyncio.sleep(min_trajectory_time)
            #     await self.fps.update_status()

            #     if self.fps.status & FPSStatus.IDLE:
            #         raise TrajectoryError("Move failed to start.")

            #     sbits = numpy.array([p.status for p in self.fps.values()])
            #     not_moving = numpy.where(sbits & PS.DISPLACEMENT_COMPLETED)
            #     if not_moving[0].any():
            #         not_moving_pids = numpy.array(list(self.fps))[not_moving[0]]

            #         # Before reporting that the positioner is not moving, check if
            #         # it's already there.
            #         await self.fps.update_position()
            #         positions = self.fps.get_positions()

            #         really_not_moving = []
            #         for pid in not_moving_pids:
            #             if pid not in self.trajectories:
            #                 continue

            #             current = positions[positions[:, 0] == pid][0, 1:]
            #             alpha = self.trajectories[pid]["alpha"][-1][0]
            #             beta = self.trajectories[pid]["beta"][-1][0]

            #             if not numpy.allclose(current - [alpha, beta], 0, atol=0.1):
            #                 really_not_moving.append(pid)

            #         if len(really_not_moving) > 0:
            #             not_moving_str = ", ".join(map(str, really_not_moving))
            #             # Should this be an error?
            #             warnings.warn(
            #                 "Some positioners do not appear to be "
            #                 f"moving: {not_moving_str}.",
            #                 JaegerUserWarning,
            #             )

            while True:

                await asyncio.sleep(1)

                if self.fps.locked:
                    raise TrajectoryError(
                        "The FPS got locked during the trajectory.",
                        self,
                    )

                await self.fps.update_status()

                if self.fps.status & FPSStatus.IDLE:
                    self.failed = False
                    break

                elapsed = time.time() - self.start_time
                if elapsed > (self.move_time + 3):
                    raise TrajectoryError(
                        "Some positioners did not complete the move.",
                        self,
                    )

            # TODO: There seems to be bug in the firmware. Sometimes when a positioner
            # fails to start its trajectory, at the end of the trajectory time it
            # does believe it has reached the commanded position, although it's still
            # at the initial position. In those cases issuing a SEND_TRAJECTORY_ABORT
            # followed by a position update seems to return correct positions.
            await self.fps.stop_trajectory()

            # The FPS says they have all stopped moving but check that they are
            # actually at their positions.
            await self.fps.update_position()
            failed_reach = False
            for pid in self.trajectories:
                alpha = self.trajectories[pid]["alpha"][-1][0]
                beta = self.trajectories[pid]["beta"][-1][0]
                current_position = numpy.array(self.fps[pid].position)
                if not numpy.allclose(current_position, [alpha, beta],
                                      atol=0.1):
                    warnings.warn(
                        f"Positioner {pid} may not have reached its destination.",
                        JaegerUserWarning,
                    )
                    failed_reach = True

            if failed_reach:
                raise TrajectoryError(
                    "Some positioners did not reach their destinations.",
                    self,
                )

        except BaseException:
            self.failed = True
            await self.fps.stop_trajectory()
            raise

        finally:
            # Not explicitely updating the positions here because save_snapshot()
            # will do that and no need to waste extra time.
            await self.fps.save_snapshot()

            if self.dump_file:
                self.dump_trajectory()

            self.end_time = time.time()
            if restart_pollers:
                self.fps.pollers.start()

        return True
示例#7
0
    async def send(self):
        """Sends the trajectory but does not start it."""

        self.move_time = 0.0

        await self.fps.stop_trajectory()
        await self.fps.stop_trajectory(clear_flags=True)

        if not await self.fps.update_status(
                positioner_ids=list(self.trajectories),
                timeout=1.0,
        ):
            self.failed = True
            raise TrajectoryError("Some positioners did not respond.", self)

        # Check that all positioners are ready to receive a new trajectory.
        for pos_id in self.trajectories:

            positioner = self.fps.positioners[pos_id]
            status = positioner.status

            if positioner.disabled:
                raise TrajectoryError(
                    f"positioner_id={pos_id} is disabled/offline but was "
                    "included in the trajectory.",
                    self,
                )

            if (positioner.flags.DATUM_ALPHA_INITIALIZED not in status
                    or positioner.flags.DATUM_BETA_INITIALIZED not in status
                    or positioner.flags.DISPLACEMENT_COMPLETED not in status):
                self.failed = True
                self.failed_positioners[pos_id] = "NOT_READY"
                raise TrajectoryError(
                    f"positioner_id={pos_id} is not ready to receive a trajectory.",
                    self,
                )

            traj_pos = self.trajectories[pos_id]

            self.n_points[pos_id] = (len(traj_pos["alpha"]),
                                     len(traj_pos["beta"]))

            # Gets maximum time for this positioner
            max_time_pos = max([
                max(list(zip(*traj_pos["alpha"]))[1]),
                max(list(zip(*traj_pos["beta"]))[1]),
            ])

            # Updates the global trajectory max time.
            if max_time_pos > self.move_time:
                self.move_time = max_time_pos

        new_traj_data = {}
        for pos_id in self.trajectories:
            data = SendNewTrajectory.get_data(
                self.n_points[pos_id][0],
                self.n_points[pos_id][1],
            )
            new_traj_data[pos_id] = data

        # Starts trajectory
        new_traj_cmd = await self.fps.send_command(
            "SEND_NEW_TRAJECTORY",
            positioner_ids=list(self.trajectories),
            data=new_traj_data,
        )

        if new_traj_cmd.status.failed or new_traj_cmd.status.timed_out:
            self.failed = True
            raise TrajectoryError("Failed sending SEND_NEW_TRAJECTORY.", self)

        start_trajectory_send_time = time.time()

        # How many points from the trajectory are we putting in each command.
        n_chunk = config["positioner"]["trajectory_data_n_points"]

        # Gets the maximum number of points for each arm for all positioners.
        max_points = numpy.max(list(self.n_points.values()), axis=0)
        max_points = {"alpha": max_points[0], "beta": max_points[1]}

        # Send chunks of size n_chunk to all the positioners in parallel.
        # Do alpha first, then beta.
        for arm in ["alpha", "beta"]:

            for jj in range(0, max_points[arm], n_chunk):

                data = {}
                send_trajectory_pids = []

                for pos_id in self.trajectories:

                    arm_chunk = self.trajectories[pos_id][arm][jj:jj + n_chunk]
                    if len(arm_chunk) == 0:
                        continue

                    send_trajectory_pids.append(pos_id)

                    positions = numpy.array(arm_chunk).astype(numpy.float64)
                    data_pos = SendTrajectoryData.calculate_positions(
                        positions)

                    data[pos_id] = data_pos

                self.data_send_cmd = await self.fps.send_command(
                    "SEND_TRAJECTORY_DATA",
                    positioner_ids=send_trajectory_pids,
                    data=data,
                )

                status = self.data_send_cmd.status
                if status.failed or status.timed_out:
                    for reply in self.data_send_cmd.replies:
                        if reply.response_code != ResponseCode.COMMAND_ACCEPTED:
                            code = reply.response_code.name
                            self.failed_positioners[reply.positioner_id] = code
                    self.failed = True
                    raise TrajectoryError(
                        "At least one SEND_TRAJECTORY_COMMAND failed.",
                        self,
                    )

        # Finalise the trajectories
        self.end_traj_cmds = await self.fps.send_command(
            "TRAJECTORY_DATA_END",
            positioner_ids=list(self.trajectories.keys()),
        )

        for cmd in self.end_traj_cmds:
            if cmd.status.failed:
                self.failed = True
                raise TrajectoryError("TRAJECTORY_DATA_END failed.", self)

            if ResponseCode.INVALID_TRAJECTORY in cmd.replies[0].response_code:
                self.failed = True
                raise TrajectoryError(
                    f"positioner_id={cmd.positioner_id} got an "
                    f"INVALID_TRAJECTORY reply.",
                    self,
                )

        self.data_send_time = time.time() - start_trajectory_send_time

        self._ready_to_start = True
        self.failed = False

        return True
示例#8
0
    def __init__(
        self,
        fps: FPS,
        trajectories: str | pathlib.Path | TrajectoryDataType,
        dump: bool | str = True,
        extra_dump_data: dict[str, Any] = {},
    ):

        self.fps = fps
        self.trajectories: TrajectoryDataType

        if self.fps.locked:
            raise FPSLockedError(
                f"FPS is locked by {fps.locked_by}. Cannot send trajectories.")

        if self.fps.moving:
            raise TrajectoryError(
                "The FPS is moving. Cannot send new trajectory.",
                self,
            )

        if isinstance(trajectories, (str, pathlib.Path)):
            path = pathlib.Path(trajectories)
            if path.suffix == ".json":
                self.trajectories = json.loads(open(path,
                                                    "r").read())["trajectory"]
            else:
                self.trajectories = cast(dict, read_yaml_file(trajectories))
        elif isinstance(trajectories, dict):
            self.trajectories = trajectories
        else:
            raise TrajectoryError("invalid trajectory data.", self)

        # List of positioners that failed receiving the trajectory and reason.
        self.failed_positioners: dict[int, str] = {}

        self.validate()

        #: Number of points sent to each positioner as a tuple ``(alpha, beta)``.
        self.n_points = {}

        #: The time required to complete the trajectory.
        self.move_time: float | None = None

        #: How long it took to send the trajectory.
        self.data_send_time: float | None = None

        self.failed = False
        self.send_new_trajectory_failed = False

        # Commands that will be sent. Mostly for inspection if the trajectory fails.
        self.data_send_cmd: Command | None = None
        self.end_traj_cmds: Command | None = None

        self.start_time: float | None = None
        self.end_time: float | None = None

        self.use_sync_line: bool = True
        self._ready_to_start = False

        self.dump_data = {
            "start_time": time.time(),
            "success": False,
            "trajectory_send_time": None,
            "trajectory_start_time": None,
            "end_time": None,
            "use_sync_line": True,
            "extra": extra_dump_data,
            "trajectory": self.trajectories,
            "initial_positions": self.fps.get_positions_dict(),
            "final_positions": {},
        }

        if dump is False:
            self.dump_file = None
        elif isinstance(dump, (str, pathlib.Path)):
            self.dump_file = str(dump)
        else:
            dirname = config["positioner"]["trajectory_dump_path"]
            mjd = str(int(Time.now().mjd))
            dirname = os.path.join(dirname, mjd)

            files = list(sorted(glob(os.path.join(dirname, "*.json"))))
            if len(files) == 0:
                seq = 1
            else:
                seq = int(files[-1].split("-")[-1].split(".")[0]) + 1

            self.dump_file = os.path.join(dirname,
                                          f"trajectory-{mjd}-{seq:04d}.json")