Source code for ophyd_async.sim._motor

import asyncio
from collections.abc import Callable
from dataclasses import dataclass
from functools import cached_property

from ophyd_async.core import (
    AsyncStatus,
    DeviceMock,
    FlyMotorInfo,
    MovableLogic,
    SignalRW,
    StandardMovable,
    StandardReadable,
    TimeoutCalculator,
    WatchableAsyncStatus,
    default_mock_class,
    error_if_none,
    simulate_move,
    soft_signal_r_and_setter,
    soft_signal_rw,
)
from ophyd_async.core import StandardReadableFormat as Format


@dataclass
class SimMotorMoveLogic(MovableLogic[float]):
    readback_set: Callable[[float], None]
    velocity: SignalRW[float]
    acceleration_time: SignalRW[float]

    async def stop(self) -> None:
        """Stop the motion."""
        await self.setpoint.set(await self.readback.get_value())

    async def move(self, new_position: float, timeout: TimeoutCalculator) -> None:
        old_position, velocity, acceleration_time = await asyncio.gather(
            self.readback.get_value(),
            self.velocity.get_value(),
            self.acceleration_time.get_value(),
        )

        await self.setpoint.set(new_position)
        async for position in simulate_move(
            old_position, new_position, velocity, acceleration_time
        ):
            self.readback_set(position)


# Remove InstantMovableMock as SimMotor owns this logic, depends if instant=True/False
[docs] @default_mock_class(DeviceMock) class SimMotor(StandardReadable, StandardMovable[float]): """For usage when simulating a motor.""" def __init__( self, name: str = "", instant: bool = True, initial_value: float = 0.0, units: str = "mm", ) -> None: """Simulate a motor, with optional velocity. :param name: name of device :param instant: whether to move instantly or calculate move time using velocity :param initial_value: initial position of the motor :param units: units of the motor position """ # Define some signals with self.add_children_as_readables(Format.HINTED_SIGNAL): self.user_readback, self._user_readback_set = soft_signal_r_and_setter( float, initial_value, units=units ) with self.add_children_as_readables(Format.CONFIG_SIGNAL): self.velocity = soft_signal_rw(float, 0 if instant else 1.0) self.acceleration_time = soft_signal_rw(float, 0.5) self.user_setpoint = soft_signal_rw(float, initial_value, units=units) # Stored in prepare self._fly_info: FlyMotorInfo | None = None # Set on kickoff(), complete when motor reaches end position self._fly_status: WatchableAsyncStatus | None = None super().__init__(name=name) @cached_property def movable_logic(self): return SimMotorMoveLogic( readback=self.user_readback, readback_set=self._user_readback_set, setpoint=self.user_setpoint, velocity=self.velocity, acceleration_time=self.acceleration_time, )
[docs] @AsyncStatus.wrap async def prepare(self, value: FlyMotorInfo): """Calculate run-up and move there, setting fly velocity when there.""" self._fly_info = value # Move to start as fast as we can await self.velocity.set(0) await self.set( value.ramp_up_start_pos(await self.acceleration_time.get_value()) ) # Set the velocity for the actual move await self.velocity.set(value.speed)
[docs] @AsyncStatus.wrap async def kickoff(self): """Begin moving motor from prepared position to final position.""" fly_info = error_if_none( self._fly_info, "Motor must be prepared before attempting to kickoff" ) acceleration_time = await self.acceleration_time.get_value() self._fly_status = self.set(fly_info.ramp_down_end_pos(acceleration_time)) # Wait for the acceleration time to ensure we are at velocity await asyncio.sleep(acceleration_time)
[docs] def complete(self) -> WatchableAsyncStatus: """Mark as complete once motor reaches completed position.""" fly_status = error_if_none(self._fly_status, "kickoff not called") return fly_status