BCDA-APS / bluesky_training

Bluesky training, including instrument package
https://bcda-aps.github.io/bluesky_training/
Other
11 stars 0 forks source link

custom device tutorial #42

Open prjemian opened 4 years ago

prjemian commented 4 years ago

per https://github.com/BCDA-APS/use_bluesky/issues/29

prjemian commented 3 years ago

move to next milestone -- might be able to satisfy this by pointing to existing examples

prjemian commented 1 year ago

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.

prjemian commented 1 year ago

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")
prjemian commented 1 year ago

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).

prjemian commented 1 year ago

Could start with more basic examples. Make note of common prefix.

Reasons for a custom Device

Here are examples from 2019 slides:

Groupings - Neat Stage 3IDD

2019 Neat stage 3ID

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:")

Aggregate custom data - User Info

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")

Modify a standard device

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

Mixin

See the EpicsAoRecord above.

prjemian commented 1 year ago

Start with the Hello, World! example, which is pure ophyd (does not involve EPICS).

prjemian commented 1 year ago

Connect EPICS contains another grouping example, which also describes the wait_for_connection() method.. This is a good second example.

prjemian commented 1 year ago

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.

prjemian commented 1 year ago

This is really a howto. Moving to that section.

prjemian commented 1 year ago

The document has more depth than a HowTo. Moving back to tutorials.

prjemian commented 6 months ago

Might be good to start with a FAQ.