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
2 changes: 1 addition & 1 deletion .github/workflows/_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Create GitHub Release
# We pin to the SHA, not the tag, for security reasons.
# https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions
uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 # v2.0.6
uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8
with:
prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }}
files: "*"
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ dependencies = [
"p4p",
"pyyaml",
"colorlog",
"pydantic>=2.0",
"pydantic-numpy",
"pydantic==1.10.17",
"scanspec",
]
dynamic = ["version"]
license.file = "LICENSE"
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