Open prjemian opened 4 years ago
move to next milestone -- might be able to satisfy this by pointing to existing examples
Suppose you have an EPICS database and IOC that creates these PVs: thing:pv1
, thing:pv2
, thing:pv3
(ignore PVs with fields for now). Notice they all have the same PV prefix thing:
so we specify that later, when creating the Python object. We create a Device class, defining Components for each of the PV suffixes as EpicsSignal.
To create ophyd code for Bluesky, this is a starting template:
from ophyd import Component, Device, EpicsSignal
class MyUserThing(Device):
pv1 = Component(EpicsSignal, "pv1")
pv2 = Component(EpicsSignal, "pv2")
pv3 = Component(EpicsSignal, "pv3")
# create the Python object:
thing = MyUserThing("thing:", name="thing")
This connects PV thing:pv1
with ophyd control thing.pv1
, and the other two as well.
That's the general picture. There are details about when to use kind="config" and when to subclass from PvPositioner and the like. But if you keep the interface simple, this is the outline to follow.
Make change as your skills allow.
One variation might be recognizing that all of the PVs are the same EPICS record type, such as EPICS ao
records. Then, these are all floating point PVs which share many extra fields. To add them, make a custom Device for the additional configuration support from apstools. Note that this relocates the signal of thing:pv1
from thing.pv1
to thing.pv1.value
(since we have changed the EpicsSignal to a EpicsAoRecord device). Like this:
from apstools.synApps import EpicsRecordDeviceCommonAll
from apstools.synApps import EpicsRecordFloatFields
from ophyd import Component, Device, EpicsSignal
class EpicsAoRecord(EpicsRecordFloatFields, EpicsRecordDeviceCommonAll):
value = Component(EpicsSignal, ".VAL")
class MyUserThing(Device):
pv1 = Component(EpicsAoRecord, "pv1")
pv2 = Component(EpicsAoRecord, "pv2")
pv3 = Component(EpicsAoRecord, "pv3")
# create the Python object:
thing = MyUserThing("thing:", name="thing")
This gives you many, many additional fields with standard names, such as:
description = Component(EpicsSignal, ".DESC", kind="config")
processing_active = Component(EpicsSignalRO, ".PACT", kind="omitted")
scanning_rate = Component(EpicsSignal, ".SCAN", kind="config")
disable_value = Component(EpicsSignal, ".DISV", kind="config")
scan_disable_input_link_value = Component(EpicsSignal, ".DISA", kind="config")
scan_disable_value_input_link = Component(EpicsSignal, ".SDIS", kind="config")
process_record = Component(EpicsSignal, ".PROC", kind="omitted", put_complete=True)
forward_link = Component(EpicsSignal, ".FLNK", kind="config")
trace_processing = Component(EpicsSignal, ".TPRO", kind="omitted")
device_type = Component(EpicsSignalRO, ".DTYP", kind="config")
alarm_status = Component(EpicsSignalRO, ".STAT", kind="config")
alarm_severity = Component(EpicsSignalRO, ".SEVR", kind="config")
new_alarm_status = Component(EpicsSignalRO, ".NSTA", kind="config")
new_alarm_severity = Component(EpicsSignalRO, ".NSEV", kind="config")
disable_alarm_severity = Component(EpicsSignal, ".DISS", kind="config")
units = Component(EpicsSignal, ".EGU", kind="config")
precision = Component(EpicsSignal, ".PREC", kind="config")
monitor_deadband = Component(EpicsSignal, ".MDEL", kind="config")
A realistic demonstration might be a heater controller simulation, such as https://github.com/epics-modules/optics/issues/10#issuecomment-1510031466 (which needs some updates).
Could start with more basic examples. Make note of common prefix.
Reasons for a custom Device
Here are examples from 2019 slides:
class NeatStage_3IDD(Device):
x = Component(EpicsMotor, "m1", labels=("NEAT stage",))
y = Component(EpicsMotor, "m2", labels=("NEAT stage",))
theta = Component(EpicsMotor, "m3", labels=("NEAT stage",))
neat_stage = NeatStage_3IDD("3idd:", name="neat_stage")
APS undulator support uses this pattern.
class ApsUndulator(Device):
"""
APS Undulator
EXAMPLE::
undulator = ApsUndulator("ID09ds:", name="undulator")
"""
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")
# ... more
Devices can be nested. For example, the dual undulator is a Device that contains an upstream (us:
) and a downstream (ds:
) undulator.
class ApsUndulatorDual(Device):
upstream = Component(ApsUndulator, "us:")
downstream = Component(ApsUndulator, "ds:")
class ExperimentInfo(Device): # from the APS General User Proposal system
GUP_number = Component(EpicsSignalRO, "ProposalNumber", string=True)
title = Component(EpicsSignalRO, "ProposalTitle", string=True)
user_name = Component(EpicsSignalRO, "UserName", string=True)
user_institution = Component(EpicsSignalRO, "UserInstitution", string=True)
user_badge_number = Component(EpicsSignalRO, "UserBadge", string=True)
user_info = ExperimentInfo("2bmS1:", name="user_info")
Sometimes, a standard device is missing a feature, such as connection with an additional field in an EPICS record. For example, the ophyd.EpicsMotor
does not connect with every field of the EPICS motor record.
class MyEpicsMotor(EpicsMotor):
steps_per_revolution = Component(EpicsSignal, ".SREV", kind="omitted")
Also see
__init__()
, stage()
, unstage()
)See the EpicsAoRecord
above.
Start with the Hello, World! example, which is pure ophyd (does not involve EPICS).
Connect EPICS contains another grouping example, which also describes the wait_for_connection()
method.. This is a good second example.
Form follows function: Integral to the implementation of a custom ophyd.Device
is the consideration of its architecture: how the control is provided and how it will be used.
This is really a howto. Moving to that section.
The document has more depth than a HowTo. Moving back to tutorials.
Might be good to start with a FAQ.
per https://github.com/BCDA-APS/use_bluesky/issues/29