Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of pmac trajectory #440

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open

Implementation of pmac trajectory #440

wants to merge 29 commits into from

Conversation

tomtrafford
Copy link
Contributor

Implements #427.

This implements a simple trajectory for a pmac. It is initialised with a PMAC, Coordinate System name and a list of pmac motors involved in the scan. On Prepare it takes a ScanSpec stack which it converts into a position array for each axis in the scan, a velocity array for each axis in the scan and one time array (shared by all axes in the scan). It adjusts the start of the scan so that there is enough time and distance for the motors to achieve their desired velocity for the first point of the scans, and stops the motors at the end of the scan.

A Pmac CS Motor class has also been defined, which takes a motor class and adds information about its Coordinate system.

It currently doesn't have the capability to turnaround or use velocity calculations.

The current test gives the pmac a line trajectory, mocking the settings of the SampleX axis on P45. This trajectory has been tested on the p45 hardware.

@tomtrafford tomtrafford requested a review from coretl July 8, 2024 14:38
src/ophyd_async/epics/pmac/_pmacIO.py Outdated Show resolved Hide resolved
src/ophyd_async/epics/pmac/_pmacTrajectory.py Outdated Show resolved Hide resolved
# Send trajectory to brick
for axis in scanAxes:
if axis != "DURATION":
self.profile_cs_name.set(self.cs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then if we add to Motor:

    def __init__(...):
        ...
        self.input_link = epics_signal_r(str, prefix + ".INP")

Then we can do somewhere above:

input_links = asyncio.gather(*[axis.input_link.get_value() for axis in scanAxes])
cs_ports = set()
cs_axes: Dict[Motor, int] = {}
for axis in scanAxes:
    input_link = await axis.input_link.get_value()
    # Split "@asyn(PORT,num)" into ["PORT", "num"]
    split = input_link.split("(")[1].rstrip(")").split(",")
    cs_ports.add(split[0].strip())
    cs_axes[axis] = int(split[1].strip())
assert len(cs_ports) == 1, "Motors in more than one CS"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this would only work for compound motors. An Axis with straight through mapping, (p45 Sample X for example) does not have a compound motor and its link is '@ASYN(PORT, num)' where PORT is the brick and num refers to its axis number on the brick rather than a CS assignment.

However from the brick and axis number I can then use '{PMAC_PREFIX}:M{num}:CsPort_RBV' and '{PMAC_PREFIX}:M{num}:CsAxis_RBV' to get the port and axis. But this feels quite long-winded...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this would only work for compound motors.

Correct

An Axis with straight through mapping, (p45 Sample X for example) does not have a compound motor and its link is '@ASYN(PORT, num)' where PORT is the brick and num refers to its axis number on the brick rather than a CS assignment.

I forgot P45 didn't have all compound axes... I guess we'll have to solve this now.

However from the brick and axis number I can then use '{PMAC_PREFIX}:M{num}:CsPort_RBV' and '{PMAC_PREFIX}:M{num}:CsAxis_RBV' to get the port and axis. But this feels quite long-winded...

Yes, that seems right. However I think I did the wrong thing in Malcolm attaching them to the motor. We should attach them to the generic PMAC object instead, in the same way as the EDM screens have an "Axes" table with deferred move, CS port and CS Axis.

I think we need to discuss this with the DeviceVector part below, but for now could you attach these somewhere to the PMAC object so you can use them in the CS code above?

Comment on lines 105 to 107
for axis in scanAxes:
if axis != "DURATION":
await self.motors[axis.lower()].set(self.initial_pos[axis])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/ophyd_async/epics/pmac/_pmacTrajectory.py Outdated Show resolved Hide resolved
tests/epics/pmac/test_pmac.py Outdated Show resolved Hide resolved
tests/epics/pmac/test_pmac.py Outdated Show resolved Hide resolved
self.cs = cs
super().__init__(prefix, cs, name=name)

async def _ramp_up_velocity_pos(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: On the motor record I refer to this as run_up_distance rather than ramp_up. I don't have a preference for which is better, but it would be nice if they're the same

src/ophyd_async/epics/pmac/_pmacIO.py Outdated Show resolved Hide resolved
src/ophyd_async/epics/pmac/_pmacTrajectory.py Outdated Show resolved Hide resolved
src/ophyd_async/epics/pmac/_pmacTrajectory.py Outdated Show resolved Hide resolved
src/ophyd_async/epics/pmac/_pmacTrajectory.py Outdated Show resolved Hide resolved
src/ophyd_async/epics/pmac/_pmacTrajectory.py Outdated Show resolved Hide resolved
Comment on lines 60 to 98
for i in range(scanSize):
if axis != "DURATION":
self.profile[cs_axes[axis] + "_velocity"].append(
(stack[0].upper[axis][i] - stack[0].lower[axis][i])
/ (stack[0].midpoints["DURATION"][i])
)
self.profile[cs_axes[axis]].append(stack[0].midpoints[axis][i])
else:
self.profile[axis.lower()].append(
int(stack[0].midpoints[axis][i] / TICK_S)
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually we should make these numpy operations so we get some speed, but this will do for now.

We also need to make sure that we put lower and midpoint points in here, but that can also be a point for later.

src/ophyd_async/epics/pmac/_pmacTrajectory.py Outdated Show resolved Hide resolved
TICK_S = 0.000001


class PmacTrajectory(Pmac, Flyable, Preparable):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please could you implement this as a TriggerLogic to be nested within a StandardFlyer? That will remove a bit of the boilerplate around kickoff and complete

@tomtrafford
Copy link
Contributor Author

Completes #494

@tomtrafford tomtrafford force-pushed the pmac-trajectory branch 2 times, most recently from 846a066 to 79d1086 Compare October 8, 2024 07:30
@tomtrafford
Copy link
Contributor Author

With the recent changes using raw motors, I have had to pass to the PMAC which axes are on the brick. '{PMAC_PREFIX}:M{num}:CsAxis_RBV' and '{PMAC_PREFIX}:M{num}:CsPort_RBV' PVs don't exist unless there is a motor in the IOC for the associated M(num).

Comment on lines 54 to 55
self.CsAxis = epics_signal_r(str, f"{prefix}:CsAxis_RBV")
self.CsPort = epics_signal_r(str, f"{prefix}:CsPort_RBV")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.CsAxis = epics_signal_r(str, f"{prefix}:CsAxis_RBV")
self.CsPort = epics_signal_r(str, f"{prefix}:CsPort_RBV")
self.cs_axis = epics_signal_r(str, f"{prefix}:CsAxis_RBV")
self.cs_port = epics_signal_r(str, f"{prefix}:CsPort_RBV")



class PmacTrajInfo(BaseModel):
spec: Spec = Field(default=None)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
spec: Spec = Field(default=None)
spec: Spec[PmacMotor | Literal["DURATION"]] = Field(default=None)

So this makes me think we need to make duration special in scanspec

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gives a pydantic error /workspaces/ophyd-async/src/ophyd_async/epics/pmac/_pmacTrajectory.py:20:11 - error: "__getitem__" method not defined on type "type" (reportIndexIssue)

Comment on lines 296 to 308
async def profile_between_points(
chunk: Frames[PmacMotor],
gap: int,
min_time: float = 0.002,
min_interval: float = 0.002,
):
"""Make consistent time and velocity arrays for each axis

Try to create velocity profiles for all axes that all arrive at
'distance' in the same time period. The profiles will contain the
following points:-

in the following description acceleration can be -ve or +ve depending
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make these _pmac_io.py and _pmac_trajectory.py please?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Make a PMAC flyer that is implemented with a Trajectory scan Device
3 participants