Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ classifiers = [
description = "Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango"
dependencies = [
"networkx>=2.0",
"numpy<2.0.0",
"numpy<3.0.0",
"packaging",
"pint",
"bluesky>=1.13.0a3",
"event_model",
"p4p",
"pyyaml",
"colorlog",
"pydantic>=2.0",
"pydantic-numpy",
"pydantic==2.8.2",
"scanspec",
]
dynamic = ["version"]
license.file = "LICENSE"
Expand Down Expand Up @@ -52,15 +52,15 @@ dev = [
"pre-commit",
"pydata-sphinx-theme>=0.12",
"pyepics>=3.4.2",
"pyside6==6.7.0",
"pyside6==6.7.2",
"pytest",
"pytest-asyncio",
"pytest-cov",
"pytest-faulthandler",
"pytest-rerunfailures",
"pytest-timeout",
"ruff",
"sphinx<7.4.0", # https://github.com/bluesky/ophyd-async/issues/459
"sphinx<8.1.0", # https://github.com/bluesky/ophyd-async/issues/459
"sphinx-autobuild",
"autodoc-pydantic",
"sphinxcontrib-mermaid",
Expand Down
2 changes: 1 addition & 1 deletion src/ophyd_async/core/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ async def _prepare(self, value: T) -> None:
self._arm_status = await self.controller.arm(
num=self._trigger_info.number,
trigger=self._trigger_info.trigger,
exposure=self._trigger_info.livetime,
exposure=self._trigger_info.livetime - self._trigger_info.deadtime,
)
self._fly_start = time.monotonic()

Expand Down
2 changes: 1 addition & 1 deletion src/ophyd_async/epics/areadetector/drivers/ad_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,6 @@ async def __call__(self) -> tuple:
shape = await asyncio.gather(
self._driver.array_size_y.get_value(),
self._driver.array_size_x.get_value(),
self._driver.data_type.get_value(),
# self._driver.data_type.get_value(),
)
return shape
7 changes: 1 addition & 6 deletions src/ophyd_async/epics/areadetector/writers/hdf_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

from .general_hdffile import _HDFDataset, _HDFFile
from .nd_file_hdf import FileWriteMode, NDFileHDF
from .nd_plugin import convert_ad_dtype_to_np


class HDFWriter(DetectorWriter):
Expand Down Expand Up @@ -72,11 +71,7 @@ async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
self._multiplier = multiplier
outer_shape = (multiplier,) if multiplier > 1 else ()
frame_shape = detector_shape[:-1] if len(detector_shape) > 0 else []
dtype_numpy = (
convert_ad_dtype_to_np(detector_shape[-1])
if len(detector_shape) > 0
else ""
)
dtype_numpy = "|u1"

# Add the main data
self._datasets = [
Expand Down
2 changes: 1 addition & 1 deletion src/ophyd_async/epics/areadetector/writers/nd_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def __init__(self, prefix: str, name: str = "") -> None:
self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire")
self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV")
self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV")
self.data_type = epics_signal_r(ADBaseDataType, prefix + "NDDataType_RBV")
# self.data_type = epics_signal_r(ADBaseDataType, prefix + "NDDataType_RBV")
self.array_counter = epics_signal_rw_rbv(int, prefix + "ArrayCounter")
# There is no _RBV for this one
self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins")
Expand Down
2 changes: 1 addition & 1 deletion src/ophyd_async/epics/motion/motor.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def __init__(self, prefix: str, name="") -> None:
self.motor_done_move = epics_signal_r(int, prefix + ".DMOV")
self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
self.high_limit_travel = epics_signal_rw(float, prefix + ".HLM")

self.output_link = epics_signal_r(str, prefix + ".OUT")
self.motor_stop = epics_signal_x(prefix + ".STOP")
# Whether set() should complete successfully or not
self._set_success = True
Expand Down
4 changes: 4 additions & 0 deletions src/ophyd_async/epics/pmac/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from ._pmacIO import Pmac
from ._pmacTrajectory import FlyTrajectoryInfo, PmacTrajectory

__all__ = ["Pmac", "PmacTrajectory", "FlyTrajectoryInfo"]
47 changes: 47 additions & 0 deletions src/ophyd_async/epics/pmac/_pmacIO.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import numpy as np
import numpy.typing as npt
from bluesky.protocols import Flyable, Preparable

from ophyd_async.core import DeviceVector, StandardReadable, SubsetEnum

from ..signal.signal import epics_signal_r, epics_signal_rw


class Pmac(StandardReadable, Flyable, Preparable):
"""Device that moves a PMAC Motor record"""

def __init__(self, prefix: str, cs="", name="") -> None:
self.time_array = epics_signal_rw(
npt.NDArray[np.float64], prefix + ":ProfileTimeArray"
)
cs_letters = "ABCUVWXYZ"
# 1 indexed CS axes so we can index into them from the compound motor input link
self.positions = DeviceVector(
{
i + 1: epics_signal_rw(
npt.NDArray[np.float64], f"{prefix}:{letter}:Positions"
)
for i, letter in enumerate(cs_letters)
}
)
self.use_axis = DeviceVector(
{
i + 1: epics_signal_rw(bool, f"{prefix}:{letter}:UseAxis")
for i, letter in enumerate(cs_letters)
}
)
self.velocities = DeviceVector(
{
i + 1: epics_signal_rw(
npt.NDArray[np.float64], f"{prefix}:{letter}:Velocities"
)
for i, letter in enumerate(cs_letters)
}
)
self.points_to_build = epics_signal_rw(int, prefix + ":ProfilePointsToBuild")
self.build_profile = epics_signal_rw(bool, prefix + ":ProfileBuild")
self.execute_profile = epics_signal_rw(bool, prefix + ":ProfileExecute")
self.scan_percent = epics_signal_r(float, prefix + ":TscanPercent_RBV")
cs_names_enum = SubsetEnum[cs]
self.profile_cs_name = epics_signal_rw(cs_names_enum, prefix + ":ProfileCsName")
self.profile_calc_vel = epics_signal_rw(bool, prefix + ":ProfileCalcVel")
186 changes: 186 additions & 0 deletions src/ophyd_async/epics/pmac/_pmacTrajectory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import time

import numpy as np
import numpy.typing as npt
from bluesky.protocols import Flyable, Preparable
from pydantic import BaseModel, Field
from scanspec.specs import Line, Path, fly

from ophyd_async.core.async_status import AsyncStatus, WatchableAsyncStatus
from ophyd_async.core.signal import observe_value
from ophyd_async.core.utils import WatcherUpdate
from ophyd_async.epics.motion import Motor
from ophyd_async.epics.pmac import Pmac

TICK_S = 0.000001


class FlyTrajectoryInfo(BaseModel):
"""Minimal set of information required to fly a trajectory:"""

#: Absolute position of the motor once it finishes accelerating to desired
#: velocity, in motor EGUs
start_position: float = Field(frozen=True)

#: Absolute position of the motor once it begins decelerating from desired
#: velocity, in EGUs
end_position: float = Field(frozen=True)

num_positions: int = Field(frozen=True)

#: Time taken for the motor to get from start_position to end_position, excluding
#: run-up and run-down, in seconds.
time_per_position: float = Field(frozen=True, gt=0)


class PmacTrajectory(Pmac, Flyable, Preparable):
"""Device that moves a PMAC Motor record"""

def __init__(self, prefix: str, cs: int, motor: Motor, name="") -> None:
# Make a dict of which motors are for which cs axis
self._fly_start: float
self.cs = cs
self.motor = motor
super().__init__(prefix, cs, name=name)

async def _ramp_up_velocity_pos(
self, velocity: float, motor: Motor, end_velocity: float
):
# Assuming ramping to or from 0
max_velocity_acceleration_time = await motor.acceleration_time.get_value()
max_velocity = await motor.max_velocity.get_value()
delta_v = abs(end_velocity - velocity)
accl_time = max_velocity_acceleration_time * delta_v / max_velocity
disp = 0.5 * (velocity + end_velocity) * accl_time
return [disp, accl_time]

@AsyncStatus.wrap
async def prepare(self, value: FlyTrajectoryInfo):
# Which Axes are in use?

spec = fly(
Line(
self.motor,
value.start_position,
value.end_position,
value.num_positions,
),
value.time_per_position,
)
stack = spec.calculate()
path = Path(stack)
chunk = path.consume()
scan_size = len(chunk)
scan_axes = chunk.axes()

cs_ports = set()
positions: dict[int, npt.NDArray[np.float64]] = {}
velocities: dict[int, npt.NDArray[np.float64]] = {}
time_array: npt.NDArray[np.float64] = []
cs_axes: dict[Motor, int] = {}
for axis in scan_axes:
if axis != "DURATION":
cs_port, cs_index = await self.get_cs_info(axis)
positions[cs_index] = []
velocities[cs_index] = []
cs_ports.add(cs_port)
cs_axes[axis] = cs_index
assert len(cs_ports) == 1, "Motors in more than one CS"
cs_port = cs_ports.pop()
self.scantime = sum(chunk.midpoints["DURATION"])

# Calc Velocity

for axis in scan_axes:
for i in range(scan_size):
if axis != "DURATION":
velocities[cs_axes[axis]].append(
(chunk.upper[axis][i] - chunk.lower[axis][i])
/ (chunk.midpoints["DURATION"][i])
)
positions[cs_axes[axis]].append(chunk.midpoints[axis][i])
else:
time_array.append(int(chunk.midpoints[axis][i] / TICK_S))

# Calculate Starting and end Position to allow ramp up and trail off velocity
self.initial_pos = {}
run_up_time = 0
final_time = 0
for axis in scan_axes:
if axis != "DURATION":
run_up_disp, run_up_t = await self._ramp_up_velocity_pos(
0,
axis,
velocities[cs_axes[axis]][0],
)
self.initial_pos[cs_axes[axis]] = (
positions[cs_axes[axis]][0] - run_up_disp
)
# trail off position and time
if velocities[cs_axes[axis]][0] == velocities[cs_axes[axis]][-1]:
final_pos = positions[cs_axes[axis]][-1] + run_up_disp
final_time = run_up_t
else:
ramp_down_disp, ramp_down_time = await self._ramp_up_velocity_pos(
velocities[cs_axes[axis]][-1],
axis,
0,
)
final_pos = positions[cs_axes[axis]][-1] + ramp_down_disp
final_time = max(ramp_down_time, final_time)
positions[cs_axes[axis]].append(final_pos)
velocities[cs_axes[axis]].append(0)
run_up_time = max(run_up_time, run_up_t)

self.scantime += run_up_time + final_time
time_array[0] += run_up_time / TICK_S
time_array.append(int(final_time / TICK_S))

for axis in scan_axes:
if axis != "DURATION":
self.profile_cs_name.set(cs_port)
self.points_to_build.set(scan_size + 1)
self.use_axis[cs_axes[axis] + 1].set(True)
self.positions[cs_axes[axis] + 1].set(positions[cs_axes[axis]])
self.velocities[cs_axes[axis] + 1].set(velocities[cs_axes[axis]])
else:
self.time_array.set(time_array)

# MOVE TO START
for axis in scan_axes:
if axis != "DURATION":
await axis.set(self.initial_pos[cs_axes[axis]])

# Set PMAC to use Velocity Array
self.profile_calc_vel.set(False)

self.build_profile.set(True)
self._fly_start = time.monotonic()

@AsyncStatus.wrap
async def kickoff(self):
self.status = self.execute_profile.set(1, timeout=self.scantime + 10)

@WatchableAsyncStatus.wrap
async def complete(self):
async for percent in observe_value(self.scan_percent):
yield WatcherUpdate(
name=self.name,
current=percent,
initial=0,
target=100,
unit="%",
precision=0,
time_elapsed=time.monotonic() - self._fly_start,
)
if percent >= 100:
break

async def get_cs_info(self, motor: Motor) -> tuple[str, int]:
output_link = await motor.output_link.get_value()
# Split "@asyn(PORT,num)" into ["PORT", "num"]
split = output_link.split("(")[1].rstrip(")").split(",")
cs_port = split[0].strip()
assert "CS" in cs_port, f"{self.name} not in a CS. It is not a compound motor."
cs_index = int(split[1].strip()) - 1
return cs_port, cs_index
12 changes: 0 additions & 12 deletions src/ophyd_async/panda/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,17 @@
PcompBlock,
PcompDirectionOptions,
PulseBlock,
SeqBlock,
TimeUnits,
)
from ._hdf_panda import HDFPanda
from ._panda_controller import PandaPcapController
from ._table import (
SeqTable,
SeqTableRow,
SeqTrigger,
seq_table_from_arrays,
seq_table_from_rows,
)
from ._trigger import (
PcompInfo,
SeqTableInfo,
StaticPcompTriggerLogic,
StaticSeqTableTriggerLogic,
)
from ._utils import phase_sorter

Expand All @@ -35,18 +29,12 @@
"EnableDisableOptions",
"PcapBlock",
"PulseBlock",
"seq_table_from_arrays",
"seq_table_from_rows",
"SeqBlock",
"SeqTableInfo",
"SeqTable",
"SeqTableRow",
"SeqTrigger",
"phase_sorter",
"PandaPcapController",
"TimeUnits",
"DataBlock",
"CommonPandABlocks",
"StaticSeqTableTriggerLogic",
"StaticPcompTriggerLogic",
]
Loading