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

Wrote new support for the planar undulator. #988

Merged
merged 2 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 1 addition & 2 deletions apstools/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@

from .aps_machine import ApsMachineParametersDevice

from .aps_undulator import ApsUndulator
from .aps_undulator import ApsUndulatorDual
from .aps_undulator import PlanarUndulator

from .area_detector_support import AD_EpicsFileNameMixin
from .area_detector_support import AD_FrameType_schemes
Expand Down
120 changes: 66 additions & 54 deletions apstools/devices/aps_undulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,81 +8,93 @@
~ApsUndulatorDual
"""

import logging
from enum import IntEnum

from ophyd import Component
from ophyd import DerivedSignal
from ophyd import Device
from ophyd import EpicsSignal
from ophyd import EpicsSignalRO
from ophyd import PVPositioner
from ophyd import Signal

from .tracking_signal import TrackingSignal

logger = logging.getLogger(__name__)

class ApsUndulator(Device):
"""
APS Undulator

.. index:: Ophyd Device; ApsUndulator
class DoneStatus(IntEnum):
MOVING = 0
DONE = 1

EXAMPLE::

undulator = ApsUndulator("ID09ds:", name="undulator")
"""
class BusyStatus(IntEnum):
prjemian marked this conversation as resolved.
Show resolved Hide resolved
DONE = 0
BUSY = 1


class MotorDriveStatus(IntEnum):
NOT_READY = 0
READY_TO_MOVE = 1


class UndulatorPositioner(PVPositioner):
"""A positioner for any of the gap control parameters.

Communicates with the parent (presumably the undulator device) to
start and stop the device.

energy = Component(
EpicsSignal,
"Energy",
write_pv="EnergySet",
put_complete=True,
kind="hinted",
)
energy_taper = Component(
EpicsSignal,
"TaperEnergy",
write_pv="TaperEnergySet",
kind="config",
)
gap = Component(EpicsSignal, "Gap", write_pv="GapSet")
gap_taper = Component(EpicsSignal, "TaperGap", write_pv="TaperGapSet", kind="config")
start_button = Component(EpicsSignal, "Start", put_complete=True, kind="omitted")
stop_button = Component(EpicsSignal, "Stop", kind="omitted")
harmonic_value = Component(EpicsSignal, "HarmonicValue", kind="config")
gap_deadband = Component(EpicsSignal, "DeadbandGap", kind="config")
device_limit = Component(EpicsSignal, "DeviceLimit", kind="config")

access_mode = Component(EpicsSignalRO, "AccessSecurity", kind="omitted")
device_status = Component(EpicsSignalRO, "Busy", kind="omitted")
total_power = Component(EpicsSignalRO, "TotalPower", kind="config")
message1 = Component(EpicsSignalRO, "Message1", kind="omitted")
message2 = Component(EpicsSignalRO, "Message2", kind="omitted")
message3 = Component(EpicsSignalRO, "Message3", kind="omitted")
time_left = Component(EpicsSignalRO, "ShClosedTime", kind="omitted")

device = Component(EpicsSignalRO, "Device", kind="config")
location = Component(EpicsSignalRO, "Location", kind="config")
version = Component(EpicsSignalRO, "Version", kind="config")

# Useful undulator parameters that are not EPICS PVs.
energy_deadband = Component(Signal, value=0.0, kind="config")
energy_backlash = Component(Signal, value=0.0, kind="config")
energy_offset = Component(Signal, value=0, kind="config")
tracking = Component(TrackingSignal, value=False, kind="config")


class ApsUndulatorDual(Device):
"""
APS Undulator with upstream *and* downstream controls

.. index:: Ophyd Device; ApsUndulatorDual
setpoint = Component(EpicsSignal, "SetC.VAL")
readback = Component(EpicsSignalRO, "M.VAL")

actuate = Component(DerivedSignal, derived_from="parent.start_button", kind="omitted")
stop_signal = Component(DerivedSignal, derived_from="parent.stop_button", kind="omitted")
done = Component(DerivedSignal, derived_from="parent.done", kind="omitted")
done_value = DoneStatus.DONE


class PlanarUndulator(Device):
"""APS Planar Undulator.

.. index:: Ophyd Device; PlanarUndulator

The signals *busy* and *done* convey complementary
information. *busy* comes from the IOC, while *done* comes
directly from the controller.

EXAMPLE::

undulator = ApsUndulatorDual("ID09", name="undulator")
undulator = PlanarUndulator("S25ID:USID:", name="undulator")

note:: the trailing ``:`` in the PV prefix should be omitted
"""

upstream = Component(ApsUndulator, "us:")
downstream = Component(ApsUndulator, "ds:")
# X-ray spectrum parameters
energy = Component(UndulatorPositioner, "Energy")
energy_taper = Component(UndulatorPositioner, "TaperEnergy")
gap = Component(UndulatorPositioner, "Gap")
gap_taper = Component(UndulatorPositioner, "TaperGap")
harmonic_value = Component(EpicsSignal, "HarmonicValueC", kind="config")
total_power = Component(EpicsSignalRO, "TotalPowerM.VAL", kind="config")
# Signals for moving the undulator
start_button = Component(EpicsSignal, "StartC.VAL", put_complete=True, kind="omitted")
stop_button = Component(EpicsSignal, "StopC.VAL", kind="omitted")
busy = Component(EpicsSignalRO, "BusyM.VAL", kind="omitted")
done = Component(EpicsSignalRO, "BusyDeviceM.VAL", kind="omitted")
motor_drive_status = Component(EpicsSignalRO, "MotorDriveStatusM.VAL", kind="omitted")
# Miscellaneous control signals
gap_deadband = Component(EpicsSignal, "DeadbandGapC", kind="config")
device_limit = Component(EpicsSignal, "DeviceLimitM.VAL", kind="config")
access_mode = Component(EpicsSignalRO, "AccessSecurityC", kind="omitted")
message1 = Component(EpicsSignalRO, "Message1M.VAL", kind="omitted")
message2 = Component(EpicsSignalRO, "Message2M.VAL", kind="omitted")
device = Component(EpicsSignalRO, "DeviceM", kind="config")
magnet = Component(EpicsSignalRO, "DeviceMagnetM", kind="config")
location = Component(EpicsSignalRO, "LocationM", kind="config")
version_plc = Component(EpicsSignalRO, "PLCVersionM.VAL", kind="config")
version_hpmu = Component(EpicsSignalRO, "HPMUVersionM.VAL", kind="config")


# -----------------------------------------------------------------------------
Expand Down
23 changes: 23 additions & 0 deletions apstools/devices/tests/test_aps_undulator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import pytest
from ophyd.sim import instantiate_fake_device

from ..aps_undulator import PlanarUndulator


@pytest.fixture()
def undulator():
undulator = instantiate_fake_device(PlanarUndulator, prefix="PSS:255ID:", name="undulator")
return undulator


def test_set_energy(undulator):
assert undulator.start_button.get() == 0
undulator.energy.set(5)
assert undulator.energy.setpoint.get() == 5
assert undulator.start_button.get() == 1


def test_stop_energy(undulator):
assert undulator.stop_button.get() == 0
undulator.stop()
assert undulator.stop_button.get() == 1